Key Takeaways
- A serverless performance test measures three things a server test does not: how often and how long cold starts are, where the platform’s concurrency and scaling limits sit, and what your downstream database or API does when hundreds of function instances hit it at once.
- Report cold starts through percentiles and a cold/warm split, not an average. Cold starts are a small share of invocations (AWS says typically under 1%), so the average hides them and the p99 shows them.
- Ramp load the way real traffic arrives, include idle gaps, and push past the limits you are about to depend on.
- The test itself is billed like production traffic. Estimate its cost before you run it.
Provider facts below were read from the vendors’ own documentation on 2026-10-23 and will change; check the linked pages before you rely on a number.
How do you performance test serverless architectures?
Performance testing serverless architectures means sending realistic traffic at the HTTP endpoint (or queue) in front of your functions and watching latency percentiles, error rates and throttling while the platform scales. The platform provisions the instances, so you do not tune servers; you test cold-start latency, scaling limits and the services your functions call. The steps: define the traffic shape, tag or detect cold starts, ramp past the expected peak, watch the downstream systems, then estimate the cost of the run.
For the general method behind each step see spike testing and endurance (soak) testing, which this guide applies to functions.
What changes when the servers are not yours?
Servers still exist; the provider runs them. What changes is what you can measure and tune. You do not size a machine, so CPU and RAM graphs of a host are not the question. The questions become:
- How long does a new execution environment take to become ready (cold start)?
- How many requests can run at once before the platform throttles, and how fast does it add capacity?
- What do the things the function calls (databases, third-party APIs, queues) do when concurrency jumps?
- What does a given load cost?
Two myths are worth correcting. Auto-scaling is not instant and not unlimited: every provider documents concurrency or scale-out limits, listed below. And “serverless means no cold starts” is wrong for the on-demand plans of all three major providers.
Cold starts: what the providers document
A cold start is the extra latency when the platform has to prepare a new execution environment before it can run your code.
AWS Lambda. The docs describe an Init phase (start extensions, bootstrap the runtime, run your static code) before the handler runs, and say Init time is charged. They state that cold starts typically occur in under 1% of invocations and range from under 100 ms to over 1 second, more common in development and test functions because those are invoked less often. Factors named: package size, amount of initialization code, and how long libraries take to set up connections. The REPORT log line carries Init Duration for a cold invocation. (docs.aws.amazon.com, Lambda execution environment lifecycle, read 2026-10-23.)
Azure Functions. The Flex Consumption plan is the recommended serverless plan for new apps and supports always-ready instances to reduce cold starts; the Premium plan uses prewarmed and always-ready instances; the legacy Consumption plan has no mitigation and cold starts are expected. The page also says hosting on Linux in a Consumption plan is retiring on 30 September 2028. (learn.microsoft.com, Azure Functions scale and hosting and Event-driven scaling, updated September 2026, read 2026-10-23.)
Google Cloud. Cloud Run (the platform under Cloud Run functions) lets you set minimum instances so a number of instances stay running even without traffic; the page calls this a best-effort target and says idle minimum instances are billed (at a lower rate when idle under request-based billing). Scaling from zero can only be triggered by a request. I did not find a page confirming minimum-instance behaviour specifically for Cloud Run functions, so check that for your runtime. (docs.cloud.google.com/run, Minimum instances and Instance autoscaling, read 2026-10-23.)
How do you measure cold starts in a load test?
Do not average. Read the latency distribution.
- Make the cold invocation identifiable. The simplest way: a module-level flag in your function that is false until the first request on that execution environment, returned as a response header such as
X-Cold-Start: true. On Lambda you can also readInit Durationfrom the REPORT lines in CloudWatch Logs, and the function’s own logs on other providers. - Record cold and warm latency into separate metrics in the load tool, and report p50, p95 and p99 for each, plus the cold-start rate.
- Compare the overall p99 with the warm p99. The gap is the cold-start cost your slowest users see.
A k6 script that does this (illustrative; adapt the URL and header name, and run a short low-load check before a big run):
import http from 'k6/http';
import { check } from 'k6';
import { Trend, Rate } from 'k6/metrics';
const coldDuration = new Trend('cold_duration', true);
const warmDuration = new Trend('warm_duration', true);
const coldRate = new Rate('cold_start_rate');
export const options = {
scenarios: {
burst_after_idle: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1s',
preAllocatedVUs: 100,
maxVUs: 500,
stages: [
{ target: 2, duration: '2m' }, // quiet traffic, environments go idle
{ target: 50, duration: '30s' }, // sudden burst
{ target: 50, duration: '3m' }, // hold
{ target: 0, duration: '30s' },
],
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
warm_duration: ['p(95)<500'],
cold_duration: ['p(99)<3000'],
},
};
export default function () {
const res = http.get('https://your-function-endpoint.example/api/ping');
const cold = res.headers['X-Cold-Start'] === 'true';
(cold ? coldDuration : warmDuration).add(res.timings.duration);
coldRate.add(cold);
check(res, { 'status is 200': (r) => r.status === 200 });
}
The thresholds are placeholders; set them from your own latency objectives. ramping-arrival-rate fixes the arrival rate rather than the number of users, which matches how events reach a function (the k6 docs call it an open-model executor: iterations start independently of how the system responds). To observe cold starts again, add a pause long enough for environments to be recycled before the next burst; the providers do not publish a fixed idle period, so find it by experiment. This is why the spike test shape (idle, then burst) is the right one.
What do cold starts look like in numbers? (illustrative)
Take a function that averages 200 ms per request and a steady 50 requests per second. By AWS’s own concurrency formula (concurrency = average requests per second x average duration in seconds), that is 50 x 0.2 = 10 concurrent executions, so after the ramp roughly 10 execution environments exist and are reused. Over a 10-minute hold there are 30,000 requests, so the cold ones are on the order of the environment count, not the request count. That is the arithmetic behind “under 1%” and the reason the p99 or p99.9, not the mean, is where cold starts appear. These figures are an example for explanation, not a measurement of any provider.
The ramp matters more than the hold. Going from 2 to 50 requests per second in 30 seconds forces the platform to add about 8 environments quickly, and each new one starts cold.
How do you find the concurrency and scaling limits?
Read the documented limits first, then design the ramp to cross them in a non-production account.
| Platform | Documented limit (2026-10-23) |
|---|---|
| AWS Lambda | Default account concurrency of 1,000 across all functions in a Region (increase on request). Each function can add 1,000 execution environments every 10 seconds. Requests per second are capped at 10 times the concurrency quota (10,000 by default). Reserved concurrency caps a function; provisioned concurrency pre-initializes environments and costs extra. |
| Azure Functions | Flex Consumption: up to 1,000 instances (default maximum 100 overall instances unless you raise it; subscription memory quotas apply first). Premium: Windows 100, Linux 20 to 100. Legacy Consumption: Windows 200, Linux 100. HTTP triggers add instances at most once per second on Consumption and Premium. |
| Google Cloud Run | Instances serve concurrent requests (default concurrency up to 80 per instance, adjustable to a maximum of 1,000, per the concurrency page). Every service has a maximum instances limit. Scale-out is described as very fast, but can be throttled in exceptional cases. |
Sources: AWS Lambda “Understanding Lambda function scaling” and “Lambda quotas”; Azure Functions “Scale and hosting” and “Event-driven scaling”; Cloud Run “Concurrency” and “About instance autoscaling” (all read 2026-10-23).
Two traps follow from those numbers. The Lambda rate cap means a function averaging 20 ms at 30,000 requests per second needs only 600 concurrent executions yet is still throttled, because 30,000 exceeds 10,000 requests per second (AWS’s own worked example). And a gateway in front can have its own limit: the Lambda quotas page itself notes that API Gateway has a default throttle of 10,000 requests per second against Lambda’s default concurrency of 1,000, so one can reject traffic before the other does.
Design the test to ramp above the expected peak and watch for throttling responses. Which status code you see depends on what sits in front of the function (a gateway, a function URL, a queue), so look at the codes you actually receive and at the provider’s throttle metric. A 429 or a burst of 5xx errors at a flat arrival rate is the signal.
What happens to your database and APIs under serverless concurrency?
This is the part most serverless tests skip, and often the first thing that breaks. Each concurrent function instance can open its own connection. A burst from 10 to 500 concurrent executions can mean hundreds of new database connections at once, against a limit set for a handful of servers.
Test for it directly:
- Run the burst scenario against the real database engine, in a staging copy sized like production, not a mock.
- Watch connection counts, query latency and errors on the database during the burst, alongside the function metrics.
- If the database saturates, the usual fixes are to cap the function’s concurrency (reserved concurrency on Lambda, a maximum instance count on Azure or Cloud Run), put a connection pooler or proxy in front of the database, or decouple the work with a queue.
Azure’s scaling documentation says this directly: limit scale-out when a downstream component such as a database has limited throughput, and load-test at expected peak traffic before lowering the default maximum, because a maximum that is too low increases latency or rejects requests. Do the same arithmetic for third-party APIs: if your function calls a partner API that allows 100 requests per second, a test at 500 per second will measure their rate limiter, not your code. Use a recorded stub for the partner in that run, and a real call in a small, separate run.
What does it cost to run the test?
Serverless bills per request and per unit of compute time, so a large test is a real charge. Estimate before you start, with the provider’s current price list (not reproduced here, since prices change).
Illustrative arithmetic for a 10-minute hold at 50 requests per second: 50 x 600 = 30,000 invocations. If the function averages 200 ms with 512 MB of memory configured, that is 30,000 x 0.2 s x 0.5 GB = 3,000 GB-seconds of compute. Multiply the invocation count and the GB-seconds by the provider’s per-request and per-GB-second prices, then add what the downstream services charge for the same traffic (database requests, data transfer, logs). Two things inflate the bill: provisioned or always-ready capacity you turn on for the test, and verbose logging at high request rates. Delete or scale down test-only capacity when you are done. On Lambda, SnapStart for Java (no extra cost) and provisioned concurrency (extra charge) are separate options; SnapStart cannot be combined with provisioned concurrency on the same version.
How do you reduce cold starts once you have measured them?
Measure first, then pick the mechanism that fits:
- Smaller initialization. AWS names package size, initialization code and connection set-up as the main contributors. Import only what you need and open connections lazily.
- Pre-initialized capacity. Lambda provisioned concurrency; Azure always-ready instances (Flex Consumption) or Premium prewarmed instances; Cloud Run minimum instances. These cost money while idle, so they suit steady production paths.
- Snapshots. Lambda SnapStart. The docs list Java 11 and later, Python 3.12 and later, and .NET 8 and later as supported, and say it works best for invocations at scale and may help less for functions invoked infrequently.
- A different plan. On Azure, move off the legacy Consumption plan to Flex Consumption.
Re-run the same scenario after each change and compare the cold-duration percentiles, not feelings.
Running the load with LoadFocus
LoadFocus is an HTTP load generator in front of your function endpoint; it has no serverless-specific features documented on its product pages, so the cold/warm split above comes from your own script and headers. What it provides, per its pages (loadfocus.com/load-testing and /k6-load-testing, read 2026-10-23): load tests from 25 cloud regions, k6 and JMeter scripts, live results with response time percentiles (50th, 90th and 99th), error charts that show when errors started and how many virtual users were active, and pass/fail thresholds on P95 or P99 response time, error rate and throughput. A multi-region run is useful here because users in different regions hit different regional function deployments. Run your k6 script from the k6 load testing page flow and watch the provider’s own metrics (CloudWatch, Azure Monitor, Cloud Monitoring) next to it for the instance-level view.
What mistakes make serverless test results misleading?
- Only steady traffic. A constant arrival rate keeps environments warm and hides cold starts. Include idle gaps and bursts.
- Reporting the average. Use percentiles; see why percentiles are more useful than averages.
- A mocked database. The real bottleneck is often connections, and a mock has none.
- Testing in a shared account. A test that exhausts the account concurrency throttles every other function in that Region. Use a separate account or reserve concurrency for the other functions first.
- Ignoring the gateway. Test through the same front door that users hit.
- Skipping the cost estimate.
Frequently Asked Questions
How is performance testing serverless different from testing servers?
You do not tune or monitor hosts. You test cold-start latency, the platform’s concurrency and scaling limits, and the downstream services your functions call, using traffic shaped like real events (idle periods and bursts) rather than a constant load.
How do I measure cold starts?
Make cold invocations identifiable, for example with a response header set on the first request of each execution environment, or read Init Duration in the Lambda REPORT log line. Record cold and warm latency as separate metrics and compare their p95 and p99.
How often do cold starts happen?
AWS states that cold starts typically occur in under 1% of Lambda invocations and range from under 100 ms to over 1 second. Your own rate depends on traffic pattern, runtime and package size, so measure it with a burst-after-idle test.
What concurrency limits should I check before a load test?
On Lambda, the account concurrency quota (1,000 by default per Region), the per-function scaling rate (1,000 environments per 10 seconds) and the requests-per-second cap (10 times the concurrency quota). On Azure Functions, the maximum instance count of your plan. On Cloud Run, the service’s maximum instances and per-instance concurrency. Check any gateway in front as well.
Does provisioned concurrency remove cold starts?
It pre-initializes a set number of execution environments, so requests that fit within that number avoid a cold start; requests beyond it can still hit one. It costs extra and, on Lambda, cannot be combined with SnapStart on the same function version.
Can a load test of serverless functions get expensive?
Yes, because usage is billed per request and compute time. Multiply the planned invocations and GB-seconds by the provider’s current prices before you run it, and include downstream services and logging.
Sources
- AWS Lambda documentation: Understanding Lambda function scaling, Lambda execution environment lifecycle, Lambda SnapStart, Lambda quotas: docs.aws.amazon.com/lambda (read 2026-10-23)
- Microsoft Learn: Azure Functions scale and hosting (updated 2026-09-18) and Event-driven scaling in Azure Functions: learn.microsoft.com/azure/azure-functions (read 2026-10-23)
- Google Cloud: Cloud Run minimum instances, concurrency and instance autoscaling: docs.cloud.google.com/run/docs (read 2026-10-23)
- LoadFocus product pages: loadfocus.com/load-testing, loadfocus.com/k6-load-testing (read 2026-10-23)