Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 2.4 of 2.8 · Intermediate · 24 min read

Performance and scalability: latency, throughput, percentiles and load

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Performance is the quality people mention first and specify worst. In this lesson you'll learn the handful of ideas and formulas that professionals use to get it right.

Picture a supermarket. How long you spend at the till is the latency: how long one piece of work takes. How many shoppers the tills serve in an hour is the throughput: how much work is done per unit of time. A good requirement states both, with the load at which they apply. Your time at the till is partly the scanning, the work itself, and partly the queue in front of you. When the tills are only half busy, you barely queue. When they're almost always busy, queues grow very long very quickly, even though each scan takes the same time.

Software works the same way. The response time a client sees is everything between sending a request and getting the answer. Part of it is waiting in a queue, because the worker is busy with someone else's request. Part of it is the service time: the work itself. Network time is added in both directions. The two parts need different remedies. A faster query cuts service time. Only more capacity or less load cuts waiting time, and under load, waiting usually dominates. A query might take five milliseconds, but wait three hundred for a free connection.

Here's why averages mislead. A search service logs a thousand response times. Nine hundred take eighty milliseconds, eighty take a hundred and fifty, fifteen take one thousand two hundred, and five take four thousand. The mean is a hundred and twenty two milliseconds, which looks fine. Professionals use percentiles instead. The fiftieth percentile, or median, is the value half of requests beat. Here it's eighty milliseconds. The ninety fifth percentile, p ninety five, is the value ninety five per cent of requests beat: a hundred and fifty milliseconds. But p ninety nine is one thousand two hundred milliseconds. And p ninety nine point nine is four thousand milliseconds. So two per cent of users wait more than a second. At three hundred searches a second, that's six people every second. A requirement written as an average would pass this system. One written in percentiles fails it, correctly.

The slow end of the distribution is called the tail. A user who loads forty resources to see one page waits for the slowest of those forty, not the average. So a rare one per cent event affects about a third of page views. In large systems, one request may fan out to a hundred servers and wait for them all. If each is slow one time in a hundred, the user's request is slow about sixty three per cent of the time.

Little's law is the most useful single formula in capacity planning. The number of requests in progress equals the arrival rate multiplied by the time each one spends inside. Let's use it. A ride-hailing trip service handles one thousand eight hundred requests a second at the evening peak. Each request keeps a database connection busy for forty milliseconds. So, on average, one thousand eight hundred times nought point nought four, which is seventy two connections, are busy at once. But six instances with ten connections each give only sixty. Requests will queue for connections, and response times will climb until timeouts start. To keep the pool at most about seventy per cent busy, you need seventy two divided by nought point seven, about a hundred and three connections, or about eighteen per instance.

Why keep that headroom? This curve shows a single-server queue with a service time of twenty milliseconds. Up to fifty per cent busy, the mean response time is just forty milliseconds. At eighty per cent busy, it's a hundred milliseconds. At ninety per cent, it doubles to two hundred. Beyond that, it rises almost vertically. Flat, then a knee, then a cliff. So never plan to run critical resources near full.

Scalability is the ability to handle more load by adding resources. Scaling up gives one machine more power. It's simple, but it runs into hard limits and rising prices. Scaling out adds more machines. There's no fixed ceiling, but the software must be designed for it. And parallelism has limits. Amdahl's law says that if ten per cent of the work must be done one step at a time, no number of processors can make the job more than ten times faster. Worse, keeping data consistent between nodes has a cost that grows quickly, so past some point, adding nodes actually reduces capacity. Look for the hidden serial parts, such as a single lock or a central coordinator.

The full section works through Amdahl's law and the Universal Scalability Law, and lists the main performance tactics. Next, in Section two point five, we look at availability and reliability. See you there.

What you need to know

Two different questions: how fast, and how much

Performance is the quality people mention first and specify worst. It covers two different questions. Latency asks how long one piece of work takes: how many milliseconds between tapping 'Pay' and seeing 'Payment accepted'. Throughput asks how much work the system completes per unit of time: 1,200 payments per second, 40,000 orders per hour. The two are related but not interchangeable. A coach carrying 50 people from London to Oxford in 100 minutes has worse latency and much better throughput than a sports car carrying two people in 60 minutes.

In software the same tension appears everywhere. Batching database writes improves throughput but makes each individual write wait for its batch. Adding a cache improves both for repeated reads but does nothing for the first. A good performance requirement therefore says which question it is about, and usually states both: 'at 1,200 payments per second (throughput), 99% complete within 800 ms (latency)'. ISO/IEC 25010 files these under performance efficiency as time behaviour, resource utilisation and capacity (Section 2.2).

Response time = service time + waiting time

When you measure latency from the client's side you get the response time: everything that happened between sending the request and receiving the answer. It is useful to split it into parts. The service time is the time a resource actually spends working on your request: parsing it, running the query, rendering the result. The waiting time is the time your request spends in a queue because the resource is busy with someone else's. Network transfer time is added on top, in both directions.

The distinction matters because the two parts respond to different remedies. Service time falls when you make the work itself cheaper: a better index, a smaller payload, a faster algorithm. Waiting time falls when you add capacity or reduce load, and it is often much larger than the service time when a system is busy. Many 'slow database' investigations end with the discovery that each query takes 5 ms but waits 300 ms for a free connection in the pool.

Where the time goes

Response time seen by a client is network time plus waiting time in the queue plus service time, plus the return network timeClientrequestserverqueueWorker (busy)responsenetworkwaiting timeservice timenetworkResponse time = network + waiting + service + network
The response time a client sees includes network transfer, waiting in a queue for a busy worker, and the service time itself.

Percentiles: why averages mislead

Response times are never a single number; they form a distribution, and it is almost always skewed. Most requests are quick, and a minority are slow because they hit a cold cache, a garbage-collection pause, a lock, a retry or an overloaded neighbour. The arithmetic mean blends the two groups into a figure that describes nobody. Professionals use percentiles instead. The 50th percentile (p50, the median) is the value half of requests beat; p95 is the value 95% of requests beat; p99 and p99.9 describe the slowest 1% and 0.1%.

The slow end of the distribution is called the tail, and it matters more than it seems. The users in the tail are often your best customers: the ones with the biggest baskets, the longest histories and the most data. A user who loads 40 resources to view one page experiences the slowest of those 40, not the average. And the chance that a 40-resource page hits at least one p99 request is 1 − 0.9940 ≈ 33%: a 'rare' 1% event affects a third of page views.

Worked example

Worked example: the mean that hides the problem

A product-search API logs 1,000 response times in one minute: 900 at 80 ms, 80 at 150 ms, 15 at 1,200 ms (a cold cache) and 5 at 4,000 ms (a timeout and retry).

Sum of all times900 × 80 + 80 × 150 + 15 × 1,200 + 5 × 4,000 = 72,000 + 12,000 + 18,000 + 20,000 = 122,000 ms
Mean122,000 ÷ 1,000 = 122 ms. Looks healthy against a 200 ms target.
p50 (median)The 500th fastest request: 80 ms
p95The 950th fastest request falls inside the 150 ms group (requests 901 to 980): 150 ms
p99The 990th falls inside the 1,200 ms group (requests 981 to 995): 1,200 ms
p99.9The 999th is in the 4,000 ms group (996 to 1,000): 4,000 ms
Who is affected20 requests in 1,000 (2%) took over a second. At 300 searches per second that is 6 users every second, or about 21,600 an hour.

The mean of 122 ms passes a 200 ms target while 2% of users wait more than a second. A scenario written as 'average below 200 ms' would pass this system.

Writing the requirement as 'p95 ≤ 200 ms and p99 ≤ 500 ms' fails it, correctly, and points the team at the cold-cache and retry paths.

Percentiles cannot be averaged. The p99 across ten servers is not the mean of their ten p99 values; it must be computed from the combined data, or approximated from merged histograms.

# Why the mean misleads: 1,000 response times from one minute of traffic
import statistics

times_ms = [80] * 900 + [150] * 80 + [1200] * 15 + [4000] * 5

mean = statistics.mean(times_ms)
cuts = statistics.quantiles(times_ms, n=1000, method="inclusive")
p50, p95, p99, p999 = cuts[499], cuts[949], cuts[989], cuts[998]
print(f"mean {mean:.0f} ms, p50 {p50:.0f}, p95 {p95:.0f}, p99 {p99:.0f}, p99.9 {p999:.0f}")
# mean 122 ms, p50 80, p95 150, p99 1200, p99.9 4000
The same calculation in a few lines of Python; the mean looks fine while p99 and p99.9 reveal the problem.

Tail latency at scale

The tail becomes the main event in large systems that use fan-out, where one user request is split into many parallel requests to other servers and the answer must wait for all of them. A search engine, a social feed or a recommendations page may consult tens or hundreds of back-end shards for every query. Jeffrey Dean and Luiz André Barroso of Google analysed this in The Tail at Scale (Communications of the ACM, 2013).

Their illustration is simple. Suppose each server answers in 10 ms typically but takes 1 second for one request in 100. A user request that waits for one server is slow 1% of the time. A user request that waits for 100 such servers is slow whenever any one of them is slow: 1 − 0.99100 ≈ 63% of the time. A rare event at one server becomes the common case for the user. Dean and Barroso report measurements from a Google service in which the 99th-percentile latency for a single leaf request was 10 ms, but the 99th percentile for waiting on all leaf requests was 140 ms.

Because you cannot eliminate every source of variability (shared hardware, background jobs, garbage collection, network queues), they argue for tail-tolerant designs, much as fault-tolerant designs accept that hardware fails. Their techniques include hedged requests (send a second copy to another replica if the first has not answered within, say, the 95th-percentile time, and use whichever returns first), tied requests that cancel each other, finer-grained partitions that can be rebalanced, temporarily excluding slow machines, and returning a good-enough result when most but not all shards have answered. In one benchmark they describe, hedging after 10 ms cut the 99.9th-percentile latency from 1,800 ms to 74 ms while sending only about 2% more requests.

Little's law: how many requests are in flight

Little's law, proved by John Little in 1961, is the most useful single formula in capacity planning. For any stable system, the average number of items inside it (L) equals the average arrival rate (λ, lambda) multiplied by the average time each item spends inside (W): L = λ × W. It holds regardless of the arrival pattern or the order of service, provided the system is stable (on average, as many items leave as arrive) and you use consistent units.

For software, L is the number of requests in progress at once (the concurrency), λ is the request rate and W is the response time. If a service receives 200 requests per second and each spends 0.25 seconds inside, then on average 200 × 0.25 = 50 requests are in progress at any moment. That number drives thread pools, connection pools, memory and licence counts. The law also works backwards: if you can hold at most 50 requests at once and each takes 0.25 s, you cannot sustain more than 50 ÷ 0.25 = 200 requests per second.

Worked example

Worked example: sizing a database connection pool with Little's law

A ride-hailing app's trip service handles 1,800 requests per second at the evening peak. Each request makes database calls that keep a connection busy for 40 ms in total. Each of the 6 service instances has a pool of 10 connections. Is that enough?

Arrival rate λ1,800 requests per second
Time holding a connection W40 ms = 0.040 s
Connections in use L = λ × W1,800 × 0.040 = 72 connections busy on average
Connections available6 instances × 10 = 60
Verdict72 > 60. Requests will queue for connections, waiting time will grow without limit at peak, and response times will climb until timeouts start.
Target with headroomAim to keep the pool at most about 70% utilised: 72 ÷ 0.7 ≈ 103 connections, so about 18 per instance
Check the database103 connections must also be acceptable to the database server; if not, reduce W (faster queries, fewer round trips) or the number of database calls per request
What if queries get faster?Cutting W to 25 ms gives L = 1,800 × 0.025 = 45 connections, which fits in the existing 60 at 75% utilisation

Little's law converts a throughput target and a latency into a concurrency figure that can be compared directly with a configured limit.

It gives averages. Bursts push the instantaneous number higher, which is why the example adds headroom rather than sizing to exactly 72.

Utilisation and queueing: why systems fall off a cliff

Utilisation (ρ, rho) is the fraction of time a resource is busy. A server that receives 40 requests per second, each needing 20 ms of service, is busy 40 × 0.020 = 0.8 of the time: 80% utilised. Intuition suggests that a resource 80% busy should be only a little slower than one 50% busy. Queueing theory says otherwise, because requests do not arrive evenly; they bunch together, and when they bunch, the later ones wait.

The simplest model is the M/M/1 queue: random arrivals, random service times and a single server. Its mean response time is W = S ÷ (1 − ρ), where S is the mean service time. At 50% utilisation, W = 2S. At 80%, W = 5S. At 90%, W = 10S. At 99%, W = 100S. The denominator shrinks towards zero as utilisation approaches 100%, so response time grows without limit. Real systems are not exactly M/M/1, and multi-server pools behave better at the same utilisation, but the shape is universal: flat, then a knee, then a cliff.

M/M/1 queue: mean response time for a 20 ms service time

Utilisation ρW = S ÷ (1 − ρ)Mean response timeOf which waiting
50%20 ÷ 0.540 ms20 ms
70%20 ÷ 0.367 ms47 ms
80%20 ÷ 0.2100 ms80 ms
90%20 ÷ 0.1200 ms180 ms
95%20 ÷ 0.05400 ms380 ms
99%20 ÷ 0.012,000 ms1,980 ms

The utilisation cliff

Mean response time against utilisation for a single queue with a 20 millisecond service time: flat at first, then rising steeply above about 80 percent80%: 100 ms90%: 200 ms50%: 40 ms0%50%100%Utilisation ρ20200400Mean response time W (ms), service time S = 20 ms
In a single-server queue, mean response time doubles between 80% and 90% utilisation and keeps rising steeply towards 100%.

The practical lessons are direct. Do not plan to run critical resources near 100% busy; leave headroom so that bursts do not push you over the knee. Watch utilisation and queue length together (Brendan Gregg's USE method checks utilisation, saturation and errors for every resource). And remember that the cliff makes small changes dangerous: at 90% utilisation, a rise in traffic of only about 6% takes you to 95% and doubles the mean response time. Chapter 9 covers queues, back pressure and capacity planning in Asynchrony, queues and back pressure.

Scalability: load, elasticity, scaling up and out

Scalability is the ability to handle more load by adding resources, ideally with capacity growing in proportion and without redesign. It only means something once you say what 'load' is. Load parameters differ between systems: requests per second for an API, concurrent connections for a chat service, the ratio of reads to writes for a social network, data volume for an analytics platform, or the number of followers of the most-followed account for a feed. A scalability scenario names the load parameter, the growth, and the response that must be preserved: 'when daily orders grow from 50,000 to 500,000, p95 checkout latency stays below 1.5 s by adding instances, with cost per order no more than 20% higher'.

Elasticity adds time: the ability to add and remove capacity automatically as load rises and falls, so that a ticketing site can grow for a sale and shrink afterwards. It is a large part of the economic case for cloud platforms. There are two basic ways to add capacity. Vertical scaling (scaling up) gives one machine more CPU, memory or faster storage; it is simple, needs no change to the software, and runs into hard limits and rising prices. Horizontal scaling (scaling out) adds more machines and spreads the work; it has no fixed ceiling but requires the software to be designed for it, with stateless instances, load balancing and partitioned data. Chapter 9 covers the strategies in detail in Scaling strategies: vertical, horizontal and the scale cube.

Amdahl's law: the limit of parallelism

Adding processors or nodes only helps with work that can be done in parallel. Amdahl's law, from Gene Amdahl's 1967 paper on the limits of parallel computing, captures this. If a fraction s of the work is serial (it must be done one step at a time) and the rest, 1 − s, can be split perfectly across N processors, the best possible speed-up is speed-up(N) = 1 ÷ (s + (1 − s) ÷ N). As N grows without limit, the speed-up approaches 1 ÷ s. With 10% serial work, no number of processors can make the job more than ten times faster.

Worked example

Worked example: a nightly billing job

A telecoms company's nightly billing run takes 8 hours on one server. Profiling shows 10% of the time is serial (loading tariffs, then writing a single summary file); the other 90% rates calls independently per customer. The run must finish in under 1.5 hours. How many servers are needed?

Serial fraction s0.10; parallel fraction 0.90
Speed-up needed8 h ÷ 1.5 h ≈ 5.33
N = 41 ÷ (0.10 + 0.90 ÷ 4) = 1 ÷ 0.325 ≈ 3.08; run time 8 ÷ 3.08 ≈ 2.6 h
N = 81 ÷ (0.10 + 0.1125) = 1 ÷ 0.2125 ≈ 4.71; run time ≈ 1.70 h
N = 161 ÷ (0.10 + 0.05625) = 1 ÷ 0.15625 = 6.4; run time = 1.25 h
Smallest N that meets the targetNeed 0.10 + 0.90 ÷ N ≤ 1 ÷ 5.33 = 0.1875, so N ≥ 0.90 ÷ 0.0875 ≈ 10.3: 11 servers give 1 ÷ (0.10 + 0.0818) ≈ 5.50, run time ≈ 1.45 h
N = 641 ÷ (0.10 + 0.0141) ≈ 8.77; run time ≈ 0.91 h
Limit as N grows1 ÷ 0.10 = 10; run time never below 0.8 h
AlternativeHalve the serial part (s = 0.05), for example by loading tariffs in parallel. Then 8 servers give 1 ÷ (0.05 + 0.11875) ≈ 5.93, run time ≈ 1.35 h: the target met with 8 servers instead of 11.

Going from 16 to 64 servers (four times the cost) cuts the run only from 1.25 h to 0.91 h. Returns diminish quickly.

Reducing the serial fraction is often far cheaper than adding hardware. Architects look for the hidden serial parts: a single lock, one database row everyone updates, a central coordinator.

The Universal Scalability Law

Amdahl's law assumes that extra processors never make things worse; the speed-up just levels off. Real distributed systems often do get worse: past some point, adding nodes reduces throughput. Neil Gunther's Universal Scalability Law (USL) models this with two penalties. Contention (α, alpha) is the cost of queueing for shared resources, the same effect as Amdahl's serial fraction. Coherency (β, beta) is the cost of keeping data consistent between nodes: every node must exchange information with every other, so this cost grows roughly with the square of the number of nodes.

The relative capacity is C(N) = N ÷ (1 + α(N − 1) + βN(N − 1)). With β = 0 it reduces to Amdahl's law. With any β above zero, capacity reaches a peak at about N = √((1 − α) ÷ β) and then falls. For α = 0.03 and β = 0.002, the peak is near √(0.97 ÷ 0.002) ≈ 22 nodes, at a capacity of about 8.6 times one node; at 32 nodes capacity has dropped back to about 8.2. Gunther describes the model in Guerrilla Capacity Planning (2007). Its practical value is that α and β can be fitted to a handful of load-test measurements, which lets you predict where scaling will stop paying off before you buy the hardware.

Linear, Amdahl and USL scaling

Relative capacity against number of nodes for linear scaling, Amdahl's law with 3 percent serial work, and the Universal Scalability Law, which peaks and then declinesLinear (ideal)Amdahl, 3% serialUSL: peaks near 22 nodes18162432Number of nodes N8162432Relative capacity C(N)
Ideal scaling is linear; Amdahl's law flattens because of serial work; the USL also includes coherency costs, so capacity peaks and then declines.

Tactics for performance: an overview

Bass, Clements and Kazman group performance tactics into two families. The first is to control resource demand: reduce how much work arrives or how much each piece costs. The second is to manage resources: make the available resources work harder or add more of them. Most real designs combine several tactics; each also has costs elsewhere, which is why they are architectural decisions.

Performance tactics and where the course covers them

TacticWhat it doesTypical cost or riskMore in
Reduce demand: rate limiting, admission control, waiting roomsCaps or smooths incoming load so the core stays below its kneeSome users are delayed or refusedChapter 9: queues and back pressure
Reduce work per request: efficient algorithms, smaller payloads, fewer round tripsCuts service timeDeveloper effort; sometimes less flexible codeChapter 0: data and algorithms
Prioritise and scheduleServes important work first (paid orders before reports)Low-priority work may starveChapter 9: resilience patterns
CachingKeeps results close to the user to avoid repeated workStale data, invalidation complexity, memory costChapter 7: caching
Concurrency and asynchronyProcesses independent work in parallel; takes slow work off the request pathHarder reasoning, ordering and consistency issuesChapter 6: communication
ReplicationAdds copies of services or data to spread reads and loadConsistency between copies; costChapter 7: replication and partitioning
Partitioning (sharding)Splits data and work so each node handles a shareCross-partition queries and rebalancing are hardChapter 7: replication and partitioning
Add resources: scale up or outRaises capacityCost; limited by serial and coherency workChapter 9: scaling strategies

Check your understanding

A service receives 500 requests per second and each takes 80 ms. How many are in progress on average?

By Little's law, L = 500 × 0.08 = 40 requests in progress.

Using the M/M/1 formula, what is the mean response time of a server with a 10 ms service time at 75% utilisation?

W = 10 ÷ (1 − 0.75) = 40 ms, of which 30 ms is waiting.

Why is p99 usually a better requirement measure than the mean?

Response-time distributions are skewed; the mean can look healthy while a meaningful share of users suffer very slow responses, and p99 exposes that tail.

If 20% of a job is serial, what is the maximum speed-up from adding processors?

1 ÷ 0.2 = 5 times, however many processors are added.

Next · 2.5Availability and reliabilityWhat availability, reliability and fault tolerance really mean, how to calculate nines, MTBF and MTTR, how components in series and parallel combine, and the tactics, RTO and RPO that achieve them.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our API averages 150 ms but users still complain it is slow. How should I measure and set targets for its performance?”