Leer en español · Lire en français · Auf Deutsch lesen
Section 9.7 of 9.8 · Advanced · 25 min read
Capacity planning and performance testing
Video lesson
4 min · narrated, with captionsListen only (audio)
Read the transcript
Welcome to Section nine point seven. Capacity planning answers one question ahead of time: will we have enough of everything when demand peaks? Performance testing provides the evidence.
Before a big wedding, a caterer estimates how many guests will come. They work out how many plates each cook can serve in an hour. They add a few spare staff in case someone is ill. They order the extra ovens weeks ahead, because they can't be bought on the day. And they hold a rehearsal at full size, to check the kitchen really copes. Software teams do exactly the same.
First, forecast demand: growth, seasonal peaks and known events. Express it at the peak, in units such as requests per second, not as a daily average. Second, measure by testing how much one instance or one database can take while still meeting its latency target. The practical limit is the knee of the response time curve, not the point where it crashes. Third, add headroom for forecast error, bursts and retries, and enough redundancy to survive losing an instance or a whole zone. Fourth, order anything with a long lead time. A payment provider may need weeks of notice to raise a limit.
The tests differ in the load they apply and the question they answer. A load test ramps up to the forecast peak and holds it, to check latency and errors. A stress test goes well beyond the peak to see how the system fails, and whether it recovers. A soak test runs for hours or days to expose slow leaks, such as memory or connections. A spike test jumps from low to very high in seconds, to test autoscaling lag and rate limiters. And a breakpoint test ramps up steadily until something breaks, to find the true limit and the first bottleneck.
How the load generator simulates users matters enormously. In a closed model, a fixed number of virtual users each send a request, wait for the reply, pause, and send again. So when the system slows down, each user waits longer, and fewer requests arrive. The load falls by itself. In an open model, requests arrive at a set rate, whether or not earlier ones have been answered. That's how real visitors to a public website behave. So when the system slows, the queue and response times keep growing. For public traffic and on-sales, a closed-model test can be dangerously optimistic.
A related trap is called coordinated omission. A test intends to send one request every ten milliseconds. At fifty seconds, the system freezes for ten seconds. A closed-loop tester waits for each reply, so it stalls too. It records one ten second sample, and about nine thousand fast ones. But on the intended schedule, about a thousand requests would have arrived during the freeze, delayed by up to ten seconds, about five on average. So the report says the ninety ninth percentile is one millisecond. Corrected, it's about nine seconds. The remedy is open-model generators that measure from when each request was meant to be sent.
Let's plan a stadium tour on-sale: a hundred and eighty thousand tickets. About four hundred thousand fans are expected in the virtual waiting room. At two and a half tickets per order, seventy two thousand orders sell out the tour. The binding constraint is the payment provider's limit of a hundred and twenty payments a second. Keeping a third for retries, and with eighty per cent of fans buying, the team admits a hundred fans a second. Each fan spends about four minutes in the purchase flow, so by Little's law, twenty four thousand are active at once. That's three thousand two hundred and fifty requests a second. At four hundred per instance, that's nine instances, or fifteen to survive losing a zone, started before half past nine. The waiting room itself takes twenty thousand requests a second, served at the CDN edge, never reaching the API tier. The tour sells out in about fifteen minutes. Doubling the servers wouldn't sell tickets faster: the scarcest shared resource sets the pace.
The full section covers test tools, realistic test data and performance budgets. Next, in Section nine point eight, the last of this chapter, we look at chaos engineering and incidents. See you there.
What you need to know
- Capacity planning forecasts peak demand, measures capacity per unit under realistic conditions, adds headroom and redundancy, and orders anything with a long lead time, including quotas and third-party limits.
- Load, stress, soak, spike and breakpoint tests answer different questions: meeting targets at peak, failure behaviour beyond it, degradation over time, reaction to sudden surges and the true limit.
- Closed workload models slow down when the system does, while open models keep arriving at a set rate; public traffic and on-sales are open, so closed-model tests can be dangerously optimistic (Schroeder, Wierman and Harchol-Balter, 2006).
- Coordinated omission (Gil Tene) hides slow responses when a tester waits for replies before sending; measure from the intended send time with open-model generators and full histograms.
- k6, Gatling, JMeter and Locust are the main open-source tools; realistic scenarios, data volumes, hot keys, dependency latency and an unloaded generator matter more than the tool.
- Performance budgets, such as Core Web Vitals thresholds or a per-hop latency allocation, are checked automatically in the delivery pipeline as fitness functions.
- The scarcest shared resource, often a third party, sets the safe admission rate for an event such as a ticket on-sale.
Capacity is a plan, not a hope
Capacity planning answers one question ahead of time: will we have enough of everything when demand peaks? Everything
is broader than servers. It includes database connections and write throughput, cache memory, message broker partitions, cloud account quotas, third-party rate limits, licences and the people on call. Cloud elasticity makes some capacity quick to add, but not all: a database primary cannot be doubled in seconds, a payment provider may need weeks of notice to raise a limit, and autoscaling (covered in Section 9.1) reacts in minutes to a step change that arrives in seconds.
The method has four steps, repeated on a regular cycle (quarterly for steady growth, per event for launches and on-sales):
The capacity planning cycle
- Forecast demandCombine organic growth (orders per day rising 4% a month), seasonality (lunch and dinner peaks, Friday nights, Christmas) and known events (a marketing campaign, a stadium on-sale). Express demand in the units the system scales by: requests per second per journey, writes per second, messages per second, at the peak, not the daily average.
- Measure capacity per unitFind, by testing, how much load one instance, one database or one partition can take while still meeting its latency SLO. The knee of the response-time curve, where queueing starts to dominate (Section 2.4), is the practical limit, not the point where it crashes.
- Add headroom and redundancySize for the forecast peak plus a margin for forecast error, bursts and retries, and so that the loss of an instance or a whole availability zone still leaves enough capacity (N+1 or zone-level redundancy).
- Plan lead times and reviewList what takes time to obtain (quota increases, reserved capacity, database upgrades, third-party limits, hardware for on-premises systems) and order it early enough. Afterwards compare actual demand with the forecast and improve the model.
Sizing arithmetic with Little's law was worked through in Section 9.1; this section concentrates on how to obtain trustworthy numbers to put into it. The most common error in capacity planning is not bad arithmetic but a per-instance capacity measured under unrealistic conditions, and the second is forgetting a shared constraint, such as a third-party limit, that no amount of headroom in your own servers can fix.
Kinds of performance test
Performance testing is the only way to measure capacity, find bottlenecks before customers do and verify that a release has not made things worse. The tests differ in the load shape they apply and the question they answer. A load test checks the expected peak; a stress test goes beyond it to see how the system fails; a soak test runs for hours to expose slow leaks; a spike test checks reactions to sudden jumps; a breakpoint test ramps up until something breaks, to find the true limit.
Performance test types
| Test | Question it answers | Load shape | Typical duration | Watch for |
|---|---|---|---|---|
| Smoke | Does the test script and environment work at all? | A few users | Minutes | Script errors, wrong environment |
| Load | Do we meet our latency and error targets at expected peak? | Ramp to forecast peak, hold | 30-60 minutes | p99 latency, error rate, resource saturation |
| Stress | How does the system fail beyond peak, and does it recover? | Ramp to 1.5-3x peak, then back down | 30-60 minutes | Graceful degradation, load shedding, recovery time |
| Soak (endurance) | Does anything degrade over time? | Normal or peak load, held | Hours to days | Memory leaks, growing queues, connection leaks, disk filling |
| Spike | Can we absorb a sudden surge? | Jump from low to very high in seconds | Minutes | Autoscaling lag, queue overflow, rate limiter behaviour |
| Breakpoint (capacity) | Where is the limit and what is the first bottleneck? | Steady ramp until failure | Until it breaks | The knee in latency, the first saturated resource |
Open and closed workload models
Load generators simulate users in one of two ways, and choosing the wrong one produces results that are not just inaccurate but dangerously optimistic. In a closed workload model the tool runs a fixed number of virtual users; each sends a request, waits for the response, pauses for a think time and repeats. If the system slows down, each virtual user sends fewer requests, so the load falls by itself. In an open workload model requests arrive at a set rate, independently of how quickly earlier ones are answered, as real visitors to a public website do: thousands of fans pressing Buy at 10:00 do not wait for each other.
Bianca Schroeder, Adam Wierman and Mor Harchol-Balter showed (Open Versus Closed: A Cautionary Tale
, NSDI 2006) that the two models can give very different response times for the same system and the same average load, and that scheduling or capacity decisions tuned under one can be wrong under the other. Internal systems with a fixed population of clients, such as a call-centre application with 300 agents, are reasonably closed. Public internet traffic, event-driven consumers fed by a queue and on-sales are open, or close to it. For such systems an open-model test is the realistic one; k6's constant-arrival-rate and ramping-arrival-rate executors, for example, implement it, while its constant-vus executor is closed.
Closed versus open
Coordinated omission
Gil Tene, of Azul Systems and the author of the HdrHistogram library, named a measurement error that affects many latency tests and monitoring tools: coordinated omission. A closed-loop load generator waits for each response before sending the next request. When the system stalls, the generator stalls with it, so the requests that real users would have sent during the stall, and that would all have been slow, are never sent and never measured. The tester coordinates
with the system it is testing and omits exactly the bad samples.
Consider a test that intends to send one request every 10 ms for 100 seconds against a service that normally answers in 1 ms. At 50 seconds the service freezes for 10 seconds (a long garbage-collection pause, say). The closed-loop tester records about 9,000 fast samples and one 10-second sample, so it reports a p99 of 1 ms and even a p99.9 of 1 ms. On the intended schedule, however, about 1,000 requests would have arrived during the freeze, waiting between 10 seconds and 10 ms, about 5 seconds on average. Ten per cent of all requests were badly delayed: the true p95 is about 5 seconds and the true p99 about 9 seconds.
What the tester misses
The remedies are to use open-model (arrival-rate) generators for open systems, to measure latency from the time a request was intended to be sent rather than when it was actually sent, to record full histograms rather than averages, and to be suspicious of any latency report whose maximum is far above its p99.9 with few samples in between. Tene's modified HTTP benchmarking tool wrk2 and HdrHistogram's correction options exist for exactly this. The same error appears in production monitoring when a client library measures only the requests it managed to send.
Tools
Four open-source tools cover most needs. Apache JMeter is the veteran: Java-based, with a graphical test-plan editor, many protocol plug-ins and thread groups that are closed by default. Gatling writes tests as code (Scala, Java and Kotlin, with JavaScript and TypeScript added more recently), is efficient at high load and has open and closed injection profiles. k6, maintained by Grafana Labs, runs JavaScript test scripts on an engine written in Go, has explicit open- and closed-model executors and pass-or-fail thresholds suited to continuous integration. Locust describes user behaviour in Python and distributes load across worker processes; its users are closed-loop. Commercial and cloud-hosted services add large-scale distributed load generation and reporting.
The tool matters less than the scenario. A good script reproduces the real mix of journeys (from production analytics: perhaps 70% browsing, 20% basket changes, 10% checkout), realistic think times, realistic data (different users, restaurants and seats, not the same one 10,000 times) and the correct workload model, and it checks responses for correctness, because a fast error page is not a successful request.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
onsale: {
executor: 'ramping-arrival-rate', // open model: arrivals per second
startRate: 100, timeUnit: '1s',
preAllocatedVUs: 2000, maxVUs: 20000,
stages: [
{ target: 4900, duration: '2m' }, // ramp to 1.5x design load
{ target: 4900, duration: '20m' }, // hold
],
},
},
thresholds: { // fail the run if missed
http_req_failed: ['rate<0.005'],
http_req_duration: ['p(99)<500'],
},
};
export default function () {
const res = http.get('https://perf.tickets.example/api/events/42/seats');
check(res, { 'status is 200': (r) => r.status === 200 });
}Realistic environments and data
A performance test is only as trustworthy as its environment. A test database with 10,000 rows whose indexes fit in memory says nothing about production with 200 million rows that do not. The checklist below catches the usual differences.
Size and shape of data: production-scale volumes and realistic distributions, including hot keys such as the most popular restaurants or the best seats, which create contention a uniform test never sees. Same building blocks: the same instance types, container limits, database configuration, connection pool sizes and network topology (including cross-zone hops). Cache state: test both warm and cold caches, because a deployment or failover empties them (Section 7.6). Dependencies: third-party services replaced by stubs that reproduce their real latency distribution and rate limits, not instant responses. Isolation: no other team's tests sharing the environment, or results are noise. The generator itself: enough load-generator capacity in the right locations; a generator running at 100% CPU measures itself.
Some organisations test in production, carefully, because no copy is ever quite like it: synthetic users and test events that are excluded from business reporting, traffic shadowing (copying real requests to a new version and discarding its responses), and gradual ramps with automatic abort conditions. This needs the same safeguards as chaos experiments: a small blast radius, a kill switch and close watching of SLOs.
Performance budgets
A performance budget is a limit that a change may not exceed, agreed in advance and checked automatically, so that performance does not erode one small regression at a time. For web front ends, budgets cover page weight, script size and the Core Web Vitals, for which Google's thresholds for a good experience, measured at the 75th percentile of page loads, are a Largest Contentful Paint of 2.5 seconds or less, an Interaction to Next Paint of 200 ms or less and a Cumulative Layout Shift of 0.1 or less.
For back ends, a latency budget divides an end-to-end SLO between the hops of a journey. The food-delivery checkout has a p99 target of 800 ms; the team allocates 20 ms to the gateway, 60 ms to the order service, 80 ms to pricing, 450 ms to payment (most of it the external card provider), 40 ms to the database, 100 ms to network and serialisation, and keeps 50 ms in reserve. Each team now knows its own target, and a change that pushes pricing to 150 ms is visible as a budget breach before it becomes an SLO breach. Strictly, percentiles of the parts do not add up to the percentile of the whole, so treat the split as an engineering allocation and verify the end-to-end figure separately.
Budgets are enforced by running a short, stable performance test in the delivery pipeline and failing the build when a threshold is crossed, as the k6 thresholds above do. This is a fitness function for performance. Pipeline tests must be repeatable more than realistic: same environment, same data, same load, so that a 10% change means a real regression and not noise.
Worked example
Worked example: a capacity plan for a ticket on-sale
This is a different, larger event from the on-sale in Section 9.3, with its own figures. A stadium tour has three nights of 60,000 seats, 180,000 tickets in all, going on sale at 10:00 on a Friday. Fans enter a virtual waiting room and are admitted to the purchase flow at a controlled rate. Load tests have measured each API instance's knee at 520 requests per second; the team plans at 400 to keep p99 under 500 ms.
| Demand forecast | 520,000 presale registrations, about 75% expected to turn up: plan for 400,000 fans in the waiting room. At an average of 2.5 tickets per order, 72,000 orders sell out the tour. |
|---|---|
| Binding constraint | The payment provider's agreed limit is 120 payments per second. With 80% of admitted fans buying and a third of the limit kept for retries and error, admit 100 fans per second (6,000 per minute): 80 payments per second. |
| Active fans (Little's law) | 100 fans/s × 240 s in the purchase flow = 24,000 fans active at once |
| API load | 100 fans/s × 25 requests per journey = 2,500 requests/s; × 1.3 for clustering of seat-map calls = 3,250 requests/s design load |
| Instances | 3,250 / 400 = 8.1, so 9 instances; to survive losing one of three zones, 9 must fit in two zones: 5 per zone × 3 = 15 instances, started before 09:30 |
| Seat-hold database | 1.5 hold attempts per fan = 150 writes/s against a measured 600 writes/s at p99 under 50 ms: 25% utilisation, but test contention on the most popular seats |
| Waiting room | 400,000 fans polling every 20 s = 20,000 requests/s, served at the CDN edge and never reaching the API tier |
| Outcome | 72,000 orders / 80 per second = 900 s: sell-out in about 15 minutes, after about 90,000 fans have been admitted; the waiting room must tell everyone else promptly |
| Lead times | Payment limit confirmed 3 weeks ahead; cloud quota raised 1 week ahead; load test at 1.5x (about 4,900 requests/s), spike and 2-hour soak tests 2 weeks ahead in a production-sized environment |
The admission rate is set by the scarcest shared resource, here the payment provider, not by how many servers the team can afford; doubling servers would not sell tickets faster.
Autoscaling is a safety net here, not the plan: a step from zero to 3,250 requests per second at 10:00:00 arrives faster than instances can start.
Check your understanding
Why does a closed-model load test tend to underestimate response times for a public website?
Because its virtual users wait for each response before sending more, so when the system slows the offered load drops, whereas real visitors keep arriving and queues grow.
A tester shows p99 of 2 ms and a maximum of 8 s with almost no samples between. What should you suspect?
Coordinated omission: the tester probably stalled with the system and failed to record the many requests that would have been delayed during the pause.
In the on-sale example, why does adding API instances not allow a faster sell-out?
Because the payment provider's 120 payments per second limit, not the API tier, caps how many fans can buy per second.
What is a soak test designed to find?
Problems that only appear over time at steady load, such as memory or connection leaks, growing queues and filling disks.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “We are preparing for a big ticket on-sale. How do I work out how many instances we need and which load tests to run beforehand?”