Latencylatency budgettargetsallocationdesignslo

Latency Budgets: Spending 200 Milliseconds on Purpose

A latency target is only useful once it is divided. Give every hop an allowance, add them up, and the conversation changes from "make it faster" to "the payment provider is spending 45% of our budget and we have 15 ms left".

▶ Run the labFollow the diagnosis

Frame the diagnosis

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

Diagnostic question
How do I turn a latency target into per-component allowances that tell me which hop is actually over its share?
Symptom
The team agreed on a 200 ms target months ago. Nobody can say whether any individual service is within budget, so every latency conversation ends in "we should all be faster".
Signal
A **per-span duration breakdown summed against the target** confirms where the budget goes. Overall p95 is the signal that misleads: it tells you the target is missed without telling you who spent the money.
SymptomSignalMeasurementHypothesisEvidenceRoot CauseChangeValidationRegression Check

Divide the target before defending it

A budget converts an aspiration into arithmetic. Below, a 200 ms target is allocated across the hops a checkout request actually crosses. The allocations sum to 185 ms, leaving 15 ms of slack — which is the number that makes the budget useful. Slack is what absorbs a slower-than-usual GC, a retried packet, a cold connection. A budget with zero slack is a target you will miss whenever anything is even slightly unusual.

Notice what the breakdown does to the discussion. "Make checkout faster" is a wish. "The payment provider has 90 ms of a 200 ms budget, so we cannot add another synchronous call without going over" is an engineering constraint that can be designed against — by moving the provider call out of the request path, by caching, or by explicitly renegotiating the target.

Budgets also expose work nobody counted. Network time, TLS, serialization and the client's own rendering are real milliseconds that rarely appear in a service dashboard. If your budget only covers server-side spans, you are budgeting a fraction of what the user experiences, which is why the user-facing target and the server-side target should be stated separately (The Half of the Budget You Cannot See From the Server).

A 200 ms p95 target for POST /checkout, allocated per hop. Slack is deliberate, not left over.ILLUSTRATIVE
Network in + TLSRTT-bound; a cross-region client blows this alone20 ms
Gateway / load balancerRouting, auth check, rate-limit lookup10 ms
Application handlerValidation, orchestration, serialization20 ms
Cache lookupAssumes a hit; a miss spends the DB allowance instead5 ms
Database queryTwo indexed queries plus connection acquisition40 ms
Payment provider45% of the whole budget, and not under our control90 ms
Remaining15 ms left

What a budget tells you to do

Once allocations exist, three questions become answerable. First: which hop is over its allowance right now? That is a comparison, not a judgement call. Second: what is the cost of a proposed change? Adding a synchronous call to a new service is no longer "a small addition" — it is a request for a share of a fixed budget, and something else must give it up. Third: what happens when a hop cannot meet its allowance? That is a design conversation about moving work out of the request path, not a performance-tuning conversation.

The comparison below is what makes budgets worth the effort. The naive framing treats a target as a single number checked at the end, which produces a diffuse "everyone optimise" response. The budgeted framing localises the overage and produces a specific decision.

The most common structural answer for an over-budget external dependency is to stop waiting for it: make the call asynchronous, return a pending state and complete out of band (The Async Job Pattern). That converts a latency problem into a state-management problem, which is often the right trade — but it is a trade, and the budget is what makes it visible as one.

A target with no allocation: everyone is responsible, so nobody is
1Target: p95 < 200ms
2Current: p95 = 340ms
3
4Action items:
5 - "optimise the checkout path"
6 - "look into database performance"
7 - "see if we can cache more"
8
9# Nobody knows which hop is over.
10# Every team believes another team is the problem.
11# Any proposed feature can claim to be "just a few ms".
The same target, allocated: the overage has an address
1Target: p95 < 200ms Actual: 340ms OVER by 140ms
2
3 network+TLS 20ms budget 22ms ok
4 gateway 10ms budget 9ms ok
5 handler 20ms budget 24ms ok (within slack)
6 cache 5ms budget 4ms ok
7 database 40ms budget 51ms over by 11ms
8 payment 90ms budget 230ms OVER by 140ms <-- the request
9
10# Decision: the provider call leaves the request path.
11# 202 + webhook on completion. Budget freed: 90ms.

The numbers did not change; the framing did. An allocated budget turns "the system is slow" into "one hop is 140 ms over its allowance and here is the structural fix", which is a decision a team can actually make and a reviewer can actually check.

Budgets are per-percentile, and they are not additive the way you hope

A subtle trap: if every hop meets its allowance at p95, the end-to-end p95 is not the sum of the p95s. The end-to-end request is slow if *any* hop is slow, and hops are rarely slow simultaneously, so summing p95s over-estimates. Conversely, budgeting each hop at p50 and expecting the total to hold at p95 under-estimates badly. The practical convention is to allocate at the percentile you actually care about, then validate the composition empirically rather than trusting the arithmetic.

The other trap is treating the budget as fixed for all users. A client 150 ms away by network round trip has already spent most of a 200 ms budget before your code runs. That is not a bug to fix in the application; it is a reason for regional deployment, edge caching, or a different target per region (Cross-Region Latency Is Physics, Not Configuration).

Finally, budgets should be revisited when the system changes shape, and they should be attached to something durable — an SLO, a design document, a dashboard annotation. A budget nobody can find is rediscovered as folklore six months later, usually during an incident.

How to allocate, and what it costs to get wrong
DecisionReasonable defaultFailure mode if you get it wrong
Which percentile to budgetThe one in your SLO — usually p95 or p99Budget at p50, and the target is missed constantly under normal variance
How much slack to leaveEnough to absorb a retry and a runtime pauseZero slack means every ordinary hiccup is an SLO breach
Whether to include client timeState server and end-to-end budgets separatelyA green server dashboard beside an unhappy user base
How to handle an uncontrolled dependencyGive it an explicit allowance and a timeout at that allowanceThe dependency silently takes the whole budget whenever it degrades
Per-region targetsSeparate budget where RTT differs materiallyDistant users permanently out of SLO for reasons no code change fixes

Key points

  • A latency target becomes actionable only when it is divided into per-hop allowances that sum to it, with deliberate slack left over.
  • Budgets localise overage: "payment is 140 ms over" is a decision; "the system is slow" is a wish.
  • A new synchronous call is a request for budget — something else must give it up.
  • End-to-end p95 is not the sum of per-hop p95s; allocate at the percentile you care about and validate the composition by measurement.
  • Client network time is part of the user's budget even though it never appears on a server dashboard.

Latency Budget Builder

Change an input and watch which number moves — and which one does not.

Spend the budget
ILLUSTRATIVE
Gateway + auth10 ms
Application20 ms
Cache5 ms
Database40 ms
External API90 ms
Network (client ↔ edge)20 ms
185 ms spent · 15 ms left

Almost nothing left. A budget with no slack has no room for a retry, a garbage collection pause, or a bad day at your dependency — and all three will happen.

Follow the diagnosis

The causal chain, hop by hop — and the readings that invite the wrong conclusion.

  1. 1
    Target → team: 200 ms p95 agreed, never allocated to components.
  2. 2
    Feature → request path: a synchronous payment-provider call is added; it fits "the system" in nobody's specific budget.
  3. 3
    Provider → request: the call runs at 230 ms p95, consuming more than the entire target on its own.
  4. 4
    Dashboard → responder: end-to-end p95 is 340 ms; each team's own dashboard looks unremarkable.
  5. 5
    Root cause → team: an unallocated budget let one uncontrolled dependency absorb it entirely, invisibly, over several releases.
What this evidence makes people conclude — wrongly
  • "Every hop is within a reasonable time, so we are fine" — reasonable is not a budget; only the sum against a target settles it.
  • "The database is over by 11 ms, let us start there" — rank by absolute overage. The 140 ms hop is the request; the 11 ms hop is rounding.
  • "We can just sum the p95s to predict the total" — that over-estimates, because hops are rarely slow at the same time. Validate empirically.
  • "Server p95 is within budget, so users are happy" — the user's budget includes network and rendering that the server never sees.

Measure, fix, validate

An optimization is not finished until the metric that motivated it has moved.

How to measure it
  • • Break a representative trace into hops and record each hop's duration at the same percentile as the target.
  • • Compute actual-versus-allowance per hop and rank by absolute overage, not by percentage.
  • • Measure the client-side total separately (browser timing or mobile instrumentation) so the untracked network and render time is visible.
  • • Track budget consumption per region where round-trip time differs materially.
What actually fixes it
  • • Allocate the target across hops explicitly, leave slack, and record the allocation next to the SLO where reviewers will find it.
  • • Move the largest over-budget hop out of the synchronous path when it is not under your control — 202 plus asynchronous completion ([[async-job-pattern]]).
  • • Set each dependency's timeout at its allowance so an over-budget dependency fails fast instead of silently spending the whole budget.
  • • Re-allocate deliberately when adding functionality; treat "a few extra milliseconds" as a withdrawal that must be funded.
How you know it worked
  • • Re-measure per-hop durations at the target percentile and confirm the sum is back under target with slack intact.
  • • Confirm the end-to-end client-side measurement improved, not only the server-side spans — a fix absorbed by queueing is not a fix.
  • • Verify the timeout behaves as designed by forcing the dependency slow in a controlled test and inspecting the user-visible result.
What it costs
  • • Budgets are governance overhead: they need an owner and periodic revision, and a stale budget is worse than none because it is trusted.
  • • Timeouts set at the allowance convert slowness into errors or partial responses, which someone must design for.
  • • Moving work out of the request path improves latency but adds asynchronous state, callbacks and a completion contract to maintain.
Stop it coming back

Accuracy

Performance numbers are conditional. These are the conditions.

What these numbers depend on
  • ILLUSTRATIVEThe 200 ms target and every allowance in it are invented to demonstrate allocation. Real budgets depend on the product, the user population and the network paths involved.
  • ENVIRONMENT-SPECIFICThe network and TLS allowance is dominated by client distance. The same application can be comfortably in budget for nearby users and hopelessly out of budget for distant ones with no code difference at all.

Misconceptions

Claim
“A latency target is a single number to check at the end.”
Reality
An unallocated target produces diffuse responsibility. Dividing it is what turns the target into per-team constraints and makes new synchronous calls visibly expensive.
Claim
“The end-to-end p95 is the sum of the per-hop p95s.”
Reality
It is generally lower, because hops are rarely slow simultaneously. Sum-of-p95 is a conservative planning bound, not a prediction; measure the composed operation to know.
Claim
“If we cannot make the dependency faster, the budget is unachievable.”
Reality
A budget makes the alternative visible: stop waiting for the dependency. Asynchronous completion, caching or degraded rendering all free the allowance without the dependency changing at all.

Apply it