Be Software Architecture Wise
Be Wise Apps

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 captions
Listen 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 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

  1. 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.
  2. 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.
  3. 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).
  4. 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

TestQuestion it answersLoad shapeTypical durationWatch for
SmokeDoes the test script and environment work at all?A few usersMinutesScript errors, wrong environment
LoadDo we meet our latency and error targets at expected peak?Ramp to forecast peak, hold30-60 minutesp99 latency, error rate, resource saturation
StressHow does the system fail beyond peak, and does it recover?Ramp to 1.5-3x peak, then back down30-60 minutesGraceful degradation, load shedding, recovery time
Soak (endurance)Does anything degrade over time?Normal or peak load, heldHours to daysMemory leaks, growing queues, connection leaks, disk filling
SpikeCan we absorb a sudden surge?Jump from low to very high in secondsMinutesAutoscaling lag, queue overflow, rate limiter behaviour
Breakpoint (capacity)Where is the limit and what is the first bottleneck?Steady ramp until failureUntil it breaksThe 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

In a closed model a fixed number of virtual users each wait for a response and think before sending again, so arrivals slow when the system slows; in an open model requests arrive at a set rate regardless, so a slow system builds a queueCLOSED MODELOPEN MODELN virtualusersSystemsendreplythink time, then send againSystem slows down:each user waits longer, so fewerrequests arrive; load falls by itselfarrivals at rate λSystemrepliesnext arrival does not wait forearlier repliesSystem slows down:arrivals continue at the same rate,so the queue and response times grow
A closed-model test politely backs off when the system struggles; real public traffic does not.

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

Timeline of a 100-second test with a 10-second stall: a closed-loop tester records one slow sample, while the intended schedule shows a thousand delayed requests, so the reported p99 of 1 ms hides a true p99 of about 9 secondsOne request every 10 ms for 100 s; the system stalls for 10 s at t = 50 sstallClosed-loop tester: what it records1 sample of 10 s, about 9,000 of 1 msIntended schedule: what users would have suffered1,000 requests delayed 0.01 to 10 s0 s25 s50 s75 s100 sReported p99: 1 msp99.9: 1 ms, max: 10 sCorrected p99: about 9 sp95: about 5 s
The tester's one slow sample hides a thousand slow user experiences.

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 });
}
A k6 open-model test for the on-sale seat map, ramping to 4,900 requests per second and failing the run if p99 exceeds 500 ms or more than 0.5% of requests fail.

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 forecast520,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 constraintThe 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 load100 fans/s × 25 requests per journey = 2,500 requests/s; × 1.3 for clustering of seat-map calls = 3,250 requests/s design load
Instances3,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 database1.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 room400,000 fans polling every 20 s = 20,000 requests/s, served at the CDN edge and never reaching the API tier
Outcome72,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 timesPayment 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.

Next · 9.8Chaos and incidentsHow to find weaknesses before users do and learn from failures that happen anyway: chaos experiments and game days, incident command, severity levels and status pages, blameless reviews, the limits of MTTR and actions that change the system.Continue →

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?”