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".
Frame the diagnosis
Performance work starts from a symptom and a signal — never from a resource dashboard.
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).
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.
1Target: p95 < 200ms2Current: p95 = 340ms3 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".1Target: p95 < 200ms Actual: 340ms OVER by 140ms2 3 network+TLS 20ms budget 22ms ok4 gateway 10ms budget 9ms ok5 handler 20ms budget 24ms ok (within slack)6 cache 5ms budget 4ms ok7 database 40ms budget 51ms over by 11ms8 payment 90ms budget 230ms OVER by 140ms <-- the request9 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.
| Decision | Reasonable default | Failure mode if you get it wrong |
|---|---|---|
| Which percentile to budget | The one in your SLO — usually p95 or p99 | Budget at p50, and the target is missed constantly under normal variance |
| How much slack to leave | Enough to absorb a retry and a runtime pause | Zero slack means every ordinary hiccup is an SLO breach |
| Whether to include client time | State server and end-to-end budgets separately | A green server dashboard beside an unhappy user base |
| How to handle an uncontrolled dependency | Give it an explicit allowance and a timeout at that allowance | The dependency silently takes the whole budget whenever it degrades |
| Per-region targets | Separate budget where RTT differs materially | Distant 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.
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.
- 1Target → team: 200 ms p95 agreed, never allocated to components.
- 2Feature → request path: a synchronous payment-provider call is added; it fits "the system" in nobody's specific budget.
- 3Provider → request: the call runs at 230 ms p95, consuming more than the entire target on its own.
- 4Dashboard → responder: end-to-end p95 is 340 ms; each team's own dashboard looks unremarkable.
- 5Root cause → team: an unallocated budget let one uncontrolled dependency absorb it entirely, invisibly, over several releases.
- • "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.
- • 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.
- • 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.
- • 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.
- • 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.
- • Encode the budget as per-hop alerts, so an individual hop going over its allowance pages before the composite SLO breaches (Burn-Rate Alerts: How Fast Is the Budget Going?).
- • Add a load-test assertion per hop, not just end to end, so a regression is attributed automatically (Load Testing: What Question Is This Test Answering?).
- • Review the budget in design review whenever a synchronous dependency is added.
Accuracy
Performance numbers are conditional. These are the conditions.
- 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.