Latencylatencydistributionservice timewait timeresponse time

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.

Follow the diagnosis

Frame the diagnosis

Performance work starts from a symptom and a signal — never from a resource dashboard.

Diagnostic question
What does "the latency of this system" actually mean — and which of the several numbers hiding behind that phrase should I be looking at?
Symptom
Support tickets say the app is slow. The service dashboard shows average latency flat at 120 ms and nobody can reproduce the complaint.
Signal
A latency **histogram** confirms or refutes it in one look: the shape shows whether 120 ms describes most requests or nobody's. The **average** is the signal that misleads here — it is a single number summarising a shape it cannot represent.
SymptomSignalMeasurementHypothesisEvidenceRoot CauseChangeValidationRegression Check

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.

A bimodal latency distribution: mean 120 ms, median 62 ms, p99 1.4 s. Cache hits in the left hump, cache misses in the right.ILLUSTRATIVE
18025
142050
265075
980100
210250
90500
2401000
2102000
20+Inf
p50 62 ms — the typical request — a cache hitmean 120 ms — lands in the valley; describes almost nobodyp95 940 ms — the slow hump beginsp99 1400 ms — cache miss plus a cold connection

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.

Same 900 ms response time, two completely different problems
ReadingService time dominatesWait time dominates
Latency vs loadRoughly flat as traffic risesRises with traffic, sharply near capacity
CPU during the slownessHigh — the work is happeningOften low — nothing is happening, things are waiting
What a profiler showsA hot function or an expensive queryThreads parked, pool waiters, lock waits
What actually helpsCheaper work: algorithm, plan, payload, fewer callsMore capacity, larger pool, less contention, less arriving work
What does not helpAdding servers to run the same slow code more timesMicro-optimising a function that already waits 90% of the time
Where to read nextComputing or Waiting?, Self Time, Total Time, and Where the CPU WentQueueing: 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.

One 900 ms request. The handler is 4% of it.
critical pathILLUSTRATIVE
0225450675900
POST /orders (client stopwatch)900 ms
Network in + TLS resume45 ms
Accept queue + worker dispatch60 ms
Handler code40 ms
Wait for DB connection210 ms
DB query25 ms
Payment provider call520 ms
Accept queue + worker dispatchPure wait time — no work happening
Handler codeThe part people instinctively optimise
Wait for DB connectionPool exhausted — Connection Pool Saturation: Waiting in Front of an Idle Database
DB queryThe query itself is fast
Payment provider callThe actual bottleneck

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.

  1. 1
    User → support: "the app is slow", with no route, no time and no reproduction.
  2. 2
    Dashboard → responder: average latency 120 ms, flat. The responder concludes there is no problem.
  3. 3
    Histogram → 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.
  4. 4
    Trace → 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.
  5. 5
    Root cause → team: the complaint is real, it is wait time, and it is not in the code anyone was about to optimise.
What this evidence makes people conclude — wrongly
  • "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.

How to measure it
  • • 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.
What actually fixes it
  • • 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.
How you know it worked
  • • 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.
What it costs
  • • 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]]).
Stop it coming back
  • 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.

What these numbers depend on
  • 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

Claim
“Latency and response time are the same thing.”
Reality
Usage varies, which is exactly why you should say which you mean. The useful split is service time (being worked on) versus wait time (queued), and their sum. A team that says "latency" while measuring only in-handler service time will consistently under-report what users experience.
Claim
“If the average is good, the service is healthy.”
Reality
The average is a poor summary of a skewed or bimodal distribution, and latency distributions are almost always both. A service with a 120 ms mean can have a p99 of 1.4 s and a real population of unhappy users.
Claim
“Optimising the slowest function is the way to reduce latency.”
Reality
Only if that function is on the critical path and accounts for a meaningful share of response time. In the waterfall above the slowest *function* is irrelevant — the request is dominated by waiting for a connection and an external provider.

Apply it

Where the depth lives

Statistics
Summary statistics of skewed distributions

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.