Latency Is a Distribution, Not a Number
The dashboard says 120 ms and users say it is slow. Both are right: "the latency" was never one number. Response time decomposes into service time and wait time, and almost every production surprise lives in the waiting.
Frame the diagnosis
Performance work starts from a symptom and a signal — never from a resource dashboard.
One number, several different questions
"Latency is 120 ms" answers no question precisely. Latency of which operation? Measured at which point — the client's stopwatch, the load balancer, or the application handler after the request was dequeued? Over what window? And which part of the distribution: the typical request, the unlucky one, or the arithmetic mean that may describe neither?
The distribution below has a mean of 120 ms and a p50 of 62 ms. Most requests finish in well under a tenth of a second; a small population finishes in more than a second. The mean sits in the empty valley between the two humps — a value that almost no individual request actually experiences. This bimodal shape is extremely common in real services, and it usually means two code paths: cache hit versus cache miss, warm connection versus new TLS handshake, fast plan versus fallback plan.
So the first move when someone says "it is slow" is not to open the average. It is to ask *which requests* and pull the shape. If the shape is bimodal, the question changes from "why is the service slow" to "what puts a request in the slow hump", which is a far more answerable question — and one Percentiles: Which One, and How Many Users Is That? and Histograms: A Distribution You Can Afford to Keep Forever give you the vocabulary for.
Service time is not response time
The single most useful decomposition in performance work: response time = wait time + service time. Service time is the interval during which the request is being actively worked on. Wait time is everything else — sitting in a socket accept queue, waiting for a worker thread, waiting for a connection from the pool, waiting for a lock, waiting behind another request on the same shard.
This matters because the two respond to completely different fixes. If service time dominates, you make the work cheaper: a better algorithm, a better query plan, less serialization (Algorithmic Cost in a Request Handler, Self Time, Total Time, and Where the CPU Went). If wait time dominates, making the work cheaper barely moves the number — you need more servers, a bigger pool, less contention, or less arriving work. Teams that skip this split spend a quarter optimising a function that accounts for 8% of response time.
The tell is easy to read once you know to look: service time is roughly stable as load rises, while wait time grows with load, and grows non-linearly near saturation. If a p99 that was fine at 200 rps is terrible at 400 rps *with the same code*, you are looking at wait time and should read Queueing: Why Systems Get Slow Before They Get Broken next, not a profiler.
| Reading | Service time dominates | Wait time dominates |
|---|---|---|
| Latency vs load | Roughly flat as traffic rises | Rises with traffic, sharply near capacity |
| CPU during the slowness | High — the work is happening | Often low — nothing is happening, things are waiting |
| What a profiler shows | A hot function or an expensive query | Threads parked, pool waiters, lock waits |
| What actually helps | Cheaper work: algorithm, plan, payload, fewer calls | More capacity, larger pool, less contention, less arriving work |
| What does not help | Adding servers to run the same slow code more times | Micro-optimising a function that already waits 90% of the time |
| Where to read next | Computing or Waiting?, Self Time, Total Time, and Where the CPU Went | Queueing: Why Systems Get Slow Before They Get Broken, Saturation: The Reading Utilization Cannot Give You, Connection Pool Saturation: Waiting in Front of an Idle Database |
Where the milliseconds actually went
Latency accumulates along a path, and the path is longer than most mental models allow. Between the user pressing a button and the byte arriving, a request crosses the network, a load balancer, an accept queue, a worker pool, the handler, one or more datastores and usually an external dependency — and each of those boundaries is a place where waiting can be introduced without any code getting slower.
The waterfall below is the same 900 ms request seen as a trace. Note that the handler itself — the code someone would instinctively go optimise — is 40 ms of a 900 ms request. The 210 ms of pool wait and the 520 ms external call are the request. This is exactly the reasoning The Critical Path Is the Only Path That Pays formalises: shortening a span that is not on the critical path changes nothing a user can feel.
A practical habit follows: before proposing any fix, be able to state where the time went as a list that sums to the total. If you cannot produce that list, you do not yet have a diagnosis — you have a suspicion, and Measure Before You Optimize exists because suspicions are wrong often enough to be expensive.
Key points
- "The latency" is never one number: name the operation, the measurement point, the window and the percentile before arguing about it.
- Response time = wait time + service time; the two have different causes, different signals and different fixes.
- Service time is roughly flat under load; wait time grows with load and explodes near saturation.
- Bimodal distributions almost always mean two code paths — find what puts a request in the slow hump.
- You do not have a diagnosis until you can list where the time went in numbers that sum to the total.
Progressive depth
Overview
Latency is how long an operation takes, measured from somewhere to somewhere. Because different requests take different amounts of time, a single number cannot describe it — latency is a distribution, and which part of that distribution you quote changes the story completely.
Practical
Work in percentiles: p50 for the typical request, p95 and p99 for the unhappy ones, always with the request volume alongside so you know how many users a percentile represents. Read the histogram shape before the summary statistics, and split by route — a service-wide number blends unrelated operations into a meaningless blur. See Percentiles: Which One, and How Many Users Is That? and The Average Was Fine and Users Were Not.
Advanced
Decompose response time into wait plus service, and treat load as a variable rather than a constant: wait time grows non-linearly as utilization approaches capacity (Queueing: Why Systems Get Slow Before They Get Broken), and a request that fans out to several dependencies inherits the slowest of them (Fan-Out: Waiting for the Slowest of Seven, Tail Latency: Why p50 Being Fine Does Not Help). At this level latency is a property of the system under load, not of the code.
Internals
The waiting has physical sources: packets crossing a network at finite speed and retransmitting on loss (Packet Loss: Duplicate ACKs, Fast Retransmit and the RTO, Congestion Control: Protecting the Network); a runnable thread waiting for a core while the scheduler runs someone else (The Scheduling Problem, Context Switching); a page fault reaching storage (Page Faults); a lock held by another transaction (Locks and Deadlocks); a garbage collector pausing the mutator (Garbage Collection: Pause, Throughput, Footprint — Pick Two). Every one of these produces wait time that no amount of algorithmic improvement in your handler will remove.
Follow the diagnosis
The causal chain, hop by hop — and the readings that invite the wrong conclusion.
- 1User → support: "the app is slow", with no route, no time and no reproduction.
- 2Dashboard → responder: average latency 120 ms, flat. The responder concludes there is no problem.
- 3Histogram → responder: the distribution is bimodal; p50 is 62 ms and p99 is 1.4 s. Roughly one request in twenty lands in the slow hump.
- 4Trace → responder: slow-hump requests spend 210 ms waiting for a pool connection and 520 ms in an external call; handler code is 40 ms either way.
- 5Root cause → team: the complaint is real, it is wait time, and it is not in the code anyone was about to optimise.
- • "The average is fine, so the service is fine" — the average is a summary of a shape, and for a bimodal shape it describes no real request.
- • "p99 is 1.4 s, so the service is broken" — with a healthy p50 and a low-traffic route, p99 may be a handful of requests per hour. Read percentiles against volume (Percentiles: Which One, and How Many Users Is That?).
- • "The handler is the request" — in the trace above the handler is 4% of the response time. Instrumenting only your own code hides the majority of it.
- • "We measured latency in the application, so we know what users experience" — the application timer starts after the accept queue, which is precisely where the wait was.
Measure, fix, validate
An optimization is not finished until the metric that motivated it has moved.
- • Pull a latency **histogram** for the operation, not an average: `histogram_quantile` over `http_request_duration_seconds_bucket` at p50, p90, p99, split by route.
- • Measure at two points and diff: the client or load balancer stopwatch versus the in-handler timer. The gap is queueing and connection time nobody instruments.
- • Split each request into wait versus service using trace spans — a span that starts late is wait; a span that runs long is service.
- • Plot p50 and p99 against request rate on the same chart. Divergence as rate climbs is the queueing signature.
- • Check whether the distribution is unimodal or bimodal before reasoning about a "typical" request at all.
- • Instrument the boundaries, not just the code: accept queue wait, pool acquisition, external calls. You cannot fix wait time you never recorded.
- • Split the fix by the decomposition — reduce arriving work or add capacity for wait time; reduce work per request for service time.
- • Attack the slow hump specifically: find the discriminator (cache miss, cold connection, fallback plan) rather than trying to move the whole distribution.
- • Report and alert on percentiles with a stated volume, never on the mean.
- • Compare the full histogram before and after, not the average: the fix should visibly shrink or remove the slow hump.
- • Confirm p50 and p99 moved in the direction you predicted; a fix that improves p50 and worsens p99 is usually a caching change with a new fallback path.
- • Re-measure at the client-side or load-balancer boundary, since an in-handler improvement can be entirely absorbed by queueing.
- • State the traffic level of the comparison — a latency improvement measured at half the load is not an improvement.
- • Histograms cost more to store and query than a single average, and bucket boundaries chosen badly give you useless quantiles.
- • Instrumenting boundaries adds spans and overhead to every request; sampling reduces cost but weakens tail visibility ([[trace-sampling]]).
- • Splitting metrics by route multiplies series count — worth it, but it is real cardinality ([[cardinality]]).
- • Alert on a percentile SLI, not an average (SLIs: Measuring What the User Actually Feels); an average-based alert will not fire for the failure mode above.
- • Keep the p50/p99 pair on the service dashboard so divergence is visible at a glance (Dashboards Built Around Questions).
- • Track the wait-time spans as their own metric so a pool or queue regression is visible without opening a trace.
Accuracy
Performance numbers are conditional. These are the conditions.
- ILLUSTRATIVEThe distribution and waterfall are constructed to show a shape that is common in practice — a bimodal cache-hit/miss split with wait time dominating. They are not measurements of any real service, and the specific millisecond values carry no authority.
- WORKLOAD-SPECIFICWhether wait or service time dominates depends entirely on the workload and the load level. The decomposition is universal; the ratio is not.
Misconceptions
Apply it
Where the depth lives
The mean is a good summary only for roughly symmetric data. Latency is right-skewed and frequently multimodal, which is why percentiles — order statistics, not moments — are the working vocabulary here.