Performance Engineering and System Design Handbook / Chapter 39
Low-Latency Request/Response Services
Compose a shallow, bounded, cancellable, and observable online request path around endpoint-specific work and failure semantics.
Preparing audio…
Audio edition
Low-Latency Request/Response Services
A mechanism can be valid in isolation and still compose badly with every mechanism around it. The archetype-scale question is harder: which mechanisms belong together for a particular class of work, and where do their contracts meet?
A low-latency request/response service is not “an API behind a load balancer.” It is a finite synchronous path from a caller’s decision to a useful, correctly interpreted response. Its behavior emerges from edge queues, connection state, compute scheduling, dependency fan-out, state freshness, retries, deployment state, and the slowest required branch. An individually sensible timeout, cache, retry, pool, and autoscaler can compose into an unbounded path.
The governing rule is:
Keep the synchronous critical path shallow, bounded, cancellable, and observable. Move nonessential work out of it only with explicit completion semantics.
At this scale, one cache or scheduler is no longer the unit of judgment. A complete service must remain explainable under skew, overload, dependency failure, cold deployment, and growth.
Start with endpoint cards, not an average API
“Twelve thousand requests per second” describes traffic volume, not workload. A search request, a product-detail read, and a stock reservation can have different payloads, state paths, service demands, objectives, and failure semantics while sharing a hostname.
Mercury API records one card per endpoint and client mode:
| field | detail read | search | reserve stock |
|---|---|---|---|
| useful unit | one valid detail response | one result page | one committed reservation outcome |
| offered rate | 5,000 requests/s | 6,000 requests/s | 1,000 attempts/s |
| payload | 1 KiB in, 8 KiB out | 2 KiB in, 14 KiB out | 1 KiB in, 2 KiB out |
| client behavior | foreground, may abandon | type-ahead, superseded often | explicit action, may retry ambiguity |
| state | price and stock required; recommendations optional | derived index | authoritative stock reservation |
| objective | 150 ms p99, normal mode | 120 ms p99, normal mode | 250 ms p99 successful outcomes |
| correctness | price version declared; stock must be current enough for display | bounded search freshness | never oversell; stable operation identity |
The population and mode qualify each objective. Mobile clients across a high-round-trip geography do not share the same transport budget as a colocated service client. A 150 ms successful-response p99 says nothing about rejected, cancelled, or timed-out attempts unless those outcome classes are also measured.
Client behavior is part of demand. Type-ahead requests are superseded; cancellation can save work. A reservation attempt remains relevant after a screen transition if its outcome may have committed. Treating both as disposable reads either wastes search work or loses payment-like outcomes.
For capacity, record demand by endpoint and state, not just count:
| endpoint | CPU-ms/request | database operations/request | egress/request | dominant risk |
|---|---|---|---|---|
| detail | 1.7 | 0.4 | 8 KiB | required-branch tail |
| search | 0.9 | 0.1 | 14 KiB | index/cache miss and network |
| reserve | 3.1 | 2.0 | 2 KiB | invariant contention and commit |
These are modeled means for the teaching fixture, not measurements. A real card carries distributions by payload class, tenant, geography, cache state, deployment generation, and outcome.
Three viable shapes—and their rejection criteria
An archetype is a family, not one diagram. Three common shapes can satisfy a low-latency service contract.
Region-local modular path
The edge routes to a regional stateless handler. The handler issues a small number of bounded parallel calls to authoritative or declared-fresh dependencies and aggregates them. This works when ownership boundaries matter and the required fan-out stays shallow.
Reject it when the endpoint must synchronously compose dozens of remote owners, when correlated branch tails consume the objective, or when partial results have no honest product meaning. Adding parallelism cannot repair an unbounded number of required branches.
Read-optimized projection path
The handler reads one versioned projection shaped for the endpoint. Authority updates the projection asynchronously; the response exposes a version or as-of boundary and can fall back to authority for selected read-after-write cases. Coalescing absorbs concurrent cold misses.
This is appropriate when read volume dominates, a freshness contract is meaningful, and rebuild/reconciliation are funded. Reject it for an admission decision that requires current authoritative stock, authorization, or money movement. “Fast local read” does not grant correctness authority.
Cell-local isolated path
Routing, compute, cache, and state ownership align within a bounded cell or tenant shard. The design reduces cross-cell fan-out and blast radius, at the cost of placement constraints, imbalance, cell movement, and duplicated operating capacity.
Use it when isolation and predictable locality justify those costs. Reject it when requests routinely cross most cells or when the routing key is unknown until after expensive global work.
The choice is conditional:
| decisive condition | modular regional | projection | cell-local |
|---|---|---|---|
| freshest authority required | bounded calls acceptable | only with fallback/version rule | good if authority is colocated |
| read-to-write ratio | moderate | high | either |
| fan-out | small and stable | collapsed into derivation | small within cell |
| isolation need | dependency bulkheads | projection generation | strongest natural boundary |
| main recovery burden | dependency recovery | rebuild and reconciliation | cell evacuation and rebalance |
Terminate the edge without moving the mystery
Edge termination owns TLS, protocol negotiation, request-size enforcement, authentication handoff, coarse admission, routing, and connection lifecycle. It should remove repeated work from the application path, not hide queueing in an opaque proxy.
Connection reuse avoids repeated handshakes and congestion warm-up, but a connection is state with lifetime, capacity, and failure behavior. Too few connections serialize work. Too many increase memory, handshake bursts, file descriptors, load-balancer state, and uneven placement. Long-lived connections can pin clients to old instances or old routing decisions.
HTTP/2 multiplexes exchanges as streams on a connection, but it retains connection-level contention and flow control. It does not make downstream pools unlimited or eliminate transport loss effects. HTTP/3 changes transport behavior but does not erase application queues, dependency fan-out, or server work. Choose a protocol from observed request/response size, concurrency, loss, intermediaries, connection churn, client support, and operational visibility—not from a promise that “multiplexing lowers latency.”
Routing needs a stability contract. Per-request balancing can spread stateless reads but harm cache locality. Affinity can preserve locality but create skew and complicate draining. Key-aware routing can align with state ownership, provided stale routes are fenced or redirected rather than allowed to reach two authorities.
At every hop, expose connection setup, reuse, active streams, flow-control stalls, resets, queue age, request size rejection, and route decision. A trace that begins after the edge has already queued for 70 ms is not an end-to-end trace.
“Stateless compute” still carries request state
A stateless handler means durable business authority is outside the process. The process still carries request context:
- stable logical-operation and attempt identities;
- caller deadline and cancellation signal;
- authentication and tenant context;
- trace and exemplar identity;
- authoritative or projection version requirements;
- degradation mode; and
- resource accounting class.
Propagate only what downstream work needs. A context object that grows without ownership becomes payload and coupling. An omitted deadline makes a descendant outlive the caller. An omitted tenant class bypasses admission. An omitted version lets a projection answer a read-after-write request with older state.
Place state according to the decision. A detail page may use a regional projection for descriptive catalog text, require a versioned price, display bounded-stale availability, and send the actual reservation to stock authority. One response can legitimately compose several state classes, but each field needs a declared source and stale behavior.
Avoid synchronous remote joins disguised as service purity. If one endpoint retrieves 25 rows and calls four owners per row, its logical elegance has created up to 100 remote opportunities for queueing, failure, and tail amplification. Batch by owner, materialize a projection, change the response shape, or admit that the objective is not feasible.
Spend 150 milliseconds once
Mercury’s detail endpoint has a modeled 150 ms p99 objective for successful normal-mode requests. The architecture allocates the budget before measuring a candidate implementation:
| segment | allocation | behavior at exhaustion |
|---|---|---|
| edge and transport | 18 ms | reject or route; record edge outcome |
| endpoint admission queue | 7 ms | reject before handler work |
| local decode, validation, context | 10 ms | fail malformed work |
| parallel dependency envelope | 55 ms | cancel optional branch; fail or use declared fallback for required branch |
| join and policy | 12 ms | finish only available valid composition |
| encode and egress | 13 ms | abort response, account transmitted bytes |
| uncertainty/recovery reserve | 35 ms | protected margin, not routine work budget |
| total | 150 ms | caller observes a classified outcome |
The required price and stock branches are modeled at 38 ms and 44 ms; recommendations are optional and cut off at 30 ms. Because the branches run in parallel, the observed dependency contribution is 44 ms, not 38 + 44 + 30. The modeled path is:
18 + 7 + 10 + max(38, 44, 30) + 12 + 13 = 104 ms
150 - 104 = 46 ms modeled margin
The allocation still reserves up to 55 ms for the branch envelope and 35 ms for uncertainty. Do not convert this arithmetic into a statistical claim. The sum of stage p99 values is not the end-to-end p99: samples differ, branches correlate, and the maximum of parallel work has its own distribution. Observe the whole request population and use correlated stage spans to explain it.
Deadline ownership runs from caller to every descendant. Each hop subtracts elapsed time and a return reserve before starting new work. A timeout bounds one wait; a deadline bounds the logical operation. When the request is cancelled, the handler must stop local work, release permits, cancel outgoing calls where safe, and prevent a late optional result from mutating the response.
Cancellation is cooperative in many application runtimes. A library signal does not necessarily interrupt a CPU loop, database query, queued task, or side effect. Validate cancellation at each boundary and count work completed after caller abandonment.
Bound concurrency where work becomes scarce
An endpoint limit, worker limit, database pool, and dependency limit protect different resources. One global semaphore cannot express all four.
Use Little’s Law as a consistency check. At 12,000 requests/s and 150 ms average residence—not p99—the system would hold about 12,000/s × 0.150 s = 1,800 requests in flight. That calculation does not set a safe limit because the mean, mix, and resource demands matter. It does expose impossible dashboards: a reported steady 12,000/s, 150 ms mean, and only 200 total in-flight requests do not describe the same boundary.
Place admission before the scarce queue. A bounded worker queue does not protect a database if every admitted handler immediately waits for a connection. Give the detail and reservation paths separate concurrency budgets when their cost and importance differ. Queue age is often more decisive than queue length because a request can already lack enough remaining deadline to finish.
Low CPU, rising latency
Mercury shows 38% application CPU while latency climbs with concurrency in a newly introduced uncached detail cohort. Unlike the all-detail average in the endpoint card, this incident cohort issues two database operations per request. The database pool has 48 connections. A database operation holds a connection for a modeled mean of 18 ms:
pool service capacity = 48 × (1,000 ms/s / 18 ms/op)
≈ 2,667 operations/s
arrival demand = 1,400 requests/s × 2 operations/request
= 2,800 operations/s
modeled utilization = 2,800 / 2,667 = 1.05
At constant assumptions, the connection queue cannot drain. Application CPU can remain low because handlers are waiting. Evidence should show pool wait time and age, checked-out connections, database service time, queries per logical request, transaction duration, and deadline remaining at checkout. Raising the pool may simply move contention into the database; it is a hypothesis to test, not the repair.
Possible repairs are to reduce operations per request, shorten transaction/connection hold time, cache a safe read, combine queries, reject earlier, or add proven database capacity. The fixture at examples/performance-engineering-system-design-handbook/part-05/request-response/ reproduces the budget, endpoint demand, and pool bound.
Thread-pool exhaustion has a similar signature when blocking work occupies every worker. Separate CPU-ready time from blocking time, keep blocking pools bounded, and ensure health/control work cannot be starved behind user work.
Cache as a response contract
A cache belongs on the path only when its key, authority, freshness, invalidation, miss behavior, and failure mode are explicit.
For each response field, decide:
- Which version or as-of point does the entry represent?
- Which request dimensions affect correctness—tenant, authorization, locale, experiment, representation, and schema?
- May stale data be served in normal, dependency-impaired, or overload mode?
- Can a caller require a minimum version after a write?
- What happens when the cache is empty, slow, corrupt, or partitioned?
Request coalescing lets concurrent misses share one fill, but the leader needs a deadline and failure policy. Followers must not wait beyond their own deadlines. A failed leader should release or transfer ownership without starting a synchronized second wave. Add randomized expiry, refresh-ahead under admission, and a cap on concurrent fills per key and dependency.
The familiar rule “a cache reduces latency” fails when the cold path is much slower and cache misses synchronize. A 99% hit ratio can still dominate p99 for a hot key after a deployment flush. Validate warm, cold, expired, invalidated, and dependency-failed states separately.
Degradation is a ladder, not improvisation
Mercury declares modes before an incident:
| mode | response | entry condition | exit evidence |
|---|---|---|---|
| full | price, stock, recommendations | all budgets healthy | normal |
| no recommendations | required fields only | optional branch tail/error gate | stable optional dependency window |
| stale price display | bounded prior price with age/version marker; no purchase decision | price read impaired and policy permits | authority freshness restored |
| essential only | minimal catalog and authoritative action links | compute/egress protection | queues below recovery threshold |
| reject before work | classified overload response | insufficient safe capacity/deadline | hysteresis plus healthy reserve |
An optional dependency must be optional in code, product meaning, and operations. If its absence causes a nil dereference, changes authorization, or makes the response misleading, the architecture cannot call it optional.
Isolation assigns separate concurrency, connection, retry, and circuit state where one dependency could consume another’s budget. A circuit breaker without admission can produce synchronized probes. A fallback that calls a second slow dependency can increase work during failure. Test the whole degradation transition, including recovery and stale-state convergence.
Reads, writes, retries, and completion
Retry-safe reads are side-effect free at the declared boundary and tolerate repeated observation. That does not make every GET safe: a handler that mutates session state, charges quota, or starts a workflow has hidden effects.
Writes need stable logical-operation identity, canonical request comparison, durable outcome state, and replay behavior. A client retry after an ambiguous reservation response must learn whether the original operation committed; a fresh attempt can oversell or duplicate an effect. Keep attempt identity distinct so traces show one logical operation with several network attempts.
Retry budgets limit additional work by endpoint, dependency, outcome, and remaining deadline. Retry only failures likely to be transient and only when enough time remains for a useful response. Jitter spreads eligible retries, but it does not repair overload. Hedging a read consumes extra capacity and can worsen tails when the system is saturated; gate it on measured benefit, idempotence, and an explicit duplicate-work ceiling.
Moving audit emission, analytics, email, or index updates out of the request path can be correct. The acknowledgment must then say what completed, the asynchronous record must be durable enough for its promise, and operators need lag, retry, poison, and reconciliation evidence. “Fire and forget” is an unnamed loss policy.
Scale the resource vector, not the request counter
Using the endpoint card means Mercury’s 12,000 requests/s consume:
detail: 5,000/s × 1.7 CPU-ms = 8,500 CPU-ms/s
search: 6,000/s × 0.9 CPU-ms = 5,400 CPU-ms/s
reserve: 1,000/s × 3.1 CPU-ms = 3,100 CPU-ms/s
total: 17,000 CPU-ms/s
The fleet’s protected operating envelope is 15,600 CPU-ms/s, so the modeled mix exceeds it by 1,400 CPU-ms/s even though “12,000 requests/s” may have been safe under a cheaper mix. CPU is only one dimension. Repeat the calculation for database operations, bytes, cache fills, locks, connections, memory residency, and dependency quotas.
Autoscaling should consume endpoint-weighted demand and leading queue/concurrency evidence, then respect provisioning delay, warm-up, and state movement. Scaling on aggregate CPU alone will not repair a fixed database pool or a hot authority key. Scaling on request count alone can scale down during a shift toward expensive reservations.
Keep failure reserve explicit. Normal capacity, one-instance loss, one-zone loss, deployment overlap, and dependency impairment are different modes. “Autoscaling will add capacity” is not a substitute when queue tolerance is 150 ms and instances need minutes to become warm.
Deployments change the path
A new instance is routable before it is necessarily ready for the latency population. It may lack compiled code, connection pools, caches, routing tables, certificates, projection generations, and runtime profiles.
Warm-up should use bounded representative work, not an uncontrolled thundering herd. Readiness gates verify required state, connection health, handler capacity, and a small latency/correctness sample. A canary compares endpoint and state cohorts, not only aggregate error rate.
Draining reverses admission before termination:
- stop new connections or streams according to protocol;
- remove the instance from new routing;
- allow bounded in-flight work to finish within its original deadline;
- cancel safe abandoned reads;
- reconcile ambiguous writes by operation identity;
- close pools without synchronized reconnect elsewhere; and
- terminate only after a declared drain ceiling.
Long-lived streams, background fills, and stuck transactions need separate ownership. A deployment that kills them at the generic process timeout can turn routine rollout into retry and reconnect storms.
Observe the endpoint and the logical operation
A useful endpoint record joins:
endpoint and client/geography class
logical operation and attempt identity
deployment generation and degradation mode
payload and response-size class
cache/projection version state
admission, queue, service, dependency, join, and egress times
deadline at entry and remaining at each child call
resource demand and pool waits
outcome: success, degraded, rejected, cancelled, timeout, ambiguous
trace exemplar linked to the same population
Histograms are split by endpoint and mode before aggregation. Exemplars link a tail bucket to a trace with queue spans, connection waits, branch overlap, cancellation propagation, and response version. Profiles answer whether service time is CPU, allocation, lock, I/O, or runtime delay. Dependency dashboards must not substitute for caller-observed latency.
Avoid unbounded metric cardinality: stable endpoint templates and declared cohorts belong in metrics; request IDs, raw URLs, and user identities belong in sampled traces or logs with appropriate controls.
Validation campaign
A low-latency design is incomplete until it predicts and tests states beyond a warm steady run.
| campaign | change | decisive evidence | stop condition |
|---|---|---|---|
| baseline | representative endpoint mix and geographies | end-to-end distributions, correctness, demand vector | population mismatch |
| mix shift | increase reservation share without total-rate change | CPU/DB demand and per-endpoint SLO | protected envelope crossed |
| synchronized miss | expire one hot key generation | fill concurrency, dependency load, follower age | authority harm or unbounded fills |
| slow dependency | inject price/stock/optional delay separately | deadline, cancellation, mode, orphan work | required invariant degraded |
| pool saturation | constrain DB connections | checkout age, DB service time, CPU | queue cannot drain |
| retry wave | inject eligible and ineligible failures | attempts/logical operation, retry tax | additional-work budget crossed |
| cold rollout | replace instances under load | warm-up distribution, reconnects, cache state | p99/error guard crossed |
| drain | remove a busy instance | accepted-after-drain, ambiguous writes, duration | stale traffic persists |
| capacity loss | remove one failure domain | admission and degradation ladder | reserve exhausted |
Run correctness assertions with the load: response versions, stock behavior, idempotent outcomes, and authorization boundaries. A fast incorrect response is a failed test.
Do not use this archetype when
- The useful result is a long-running job whose honest contract is accepted, durable, progressing, and retrievable rather than synchronously complete.
- The workload is a continuous stream where event-time, backlog, checkpoint, and replay semantics dominate a caller deadline.
- The operation’s central problem is a multi-record invariant and durable commit; use the transactional archetype and treat latency within that boundary.
- The response requires an unbounded query or analytical scan; make it asynchronous, constrain it, or use an analytical archetype.
- Intermittent clients need offline ownership and later reconciliation rather than a continuously available synchronous authority.
Calling any of those systems “an API” does not make request/response their governing architecture.
Reference low-latency service checklist
- Is each endpoint population defined by mix, payload, geography, client behavior, state, and outcome?
- Is there one end-to-end objective and one budget owner, with reserve protected?
- Are required and optional branches explicit, shallow, and bounded?
- Do deadline and cancellation propagate to local work, pools, and descendants?
- Is admission placed before every scarce queue, with queue age visible?
- Are cache keys, versions, stale behavior, coalescing, and leader failure specified?
- Are write retries tied to a durable logical-operation identity?
- Does degradation preserve correctness invariants and declare recovery gates?
- Is capacity modeled by endpoint service demand across all dominant resources?
- Are cold start, warm-up, connection churn, and drain tested under load?
- Can an endpoint tail exemplar expose edge time, pool wait, branch overlap, mode, and version?
- Does the asynchronous work moved off-path have completion, lag, poison, and reconciliation semantics?
Design and diagnostic work
Field design — defend 150 ms. Reproduce the budget and 104 ms modeled path. Add 18 ms to the stock branch and 9 ms to edge transport. Decide whether to spend reserve, change the response contract, relocate state, or reject the objective. Produce three alternatives and state the evidence that would distinguish them.
Field diagnosis — low CPU, high concurrency. Start with the 48-connection, 18 ms/operation, 2,800 operations/s packet. Rank connection starvation, database slowdown, thread starvation, and downstream flow control. Request one discriminating observation for each. Explain why increasing the pool may worsen the true constraint.
Principal red team — synchronized cold path. Assume a regional cache generation expires during a rollout while price is degraded and clients retry. Specify fill ownership, admission, stale rules, retry budget, deployment halt, and recovery hysteresis. Reject any answer that lets optional enrichment weaken price or stock semantics.
Durable rules for a bounded request path
- Model endpoints and client behavior separately; aggregate request count is not workload.
- Allocate one end-to-end budget and observe its joint distribution rather than adding stage percentiles.
- Keep required fan-out small; parallel branches still amplify tail and resource demand.
- Bound queues at the scarce resource and reject work that cannot finish within its remaining deadline.
- Propagate cancellation, then prove that application work and descendants actually stop.
- Treat caches and projections as versioned response contracts, not transparent speed layers.
- Degrade optional value through declared modes; never degrade an invariant by accident.
- Make write retries replay a logical outcome, not create a fresh effect.
- Scale endpoint service-demand vectors and preserve failure/warm-up reserve.
- Validate cold, skewed, overloaded, failed, recovering, deploying, and draining states.
The synchronous path is now bounded. The next archetype changes the center of gravity: Ledgerline’s reservation path is not primarily a fan-out problem. Its architecture must serialize or safely decompose a business invariant, produce a durable acknowledgment, and remain operable under contention, plan change, migration, and restore.
Evidence and transfer limits
- RFC 9113 specifies HTTP/2 streams, multiplexing, flow control, and connection-level interaction. It supports the protocol discussion, not a claim that HTTP/2 will improve a particular endpoint; intermediaries, implementations, payloads, loss, and workloads require measurement.
- The current gRPC guides for deadlines and cancellation explain deadline propagation and cooperative cancellation behavior in that ecosystem. Language support differs, and these guides do not prove Mercury’s application work, queries, or side effects stop.
- Dean and Barroso’s The Tail at Scale establishes how component variability and fan-out influence large online-service tails and surveys mitigation techniques. Its reported systems are not sizing evidence for Mercury.
- The deterministic fixture at
examples/performance-engineering-system-design-handbook/part-05/request-response/verifies the teaching arithmetic. It assumes fixed mean service times, perfect branch parallelism, constant mix, no skew, and no retry, scheduling, runtime, network, or failure overhead. Replace it with correlated endpoint traces and measured service-demand distributions before making a production decision.
Continue reading
Full table of contents