Subscribe for more posts like this →

Understanding and Managing Tail Latencies

Your dashboard shows P99 latency at 200ms. You think: one percent of users have a bad experience. That number is correct and nearly useless. The moment your system makes multiple backend calls to serve a single request — and nearly every system above a certain complexity does — the per-service P99 stops describing the user's experience. It describes a component in isolation. The user experiences the composition.


The Composition Problem

If a single backend call has P99 of 200ms, the probability that it completes within 200ms is 0.99. With twenty parallel calls, the probability that all complete within 200ms is 0.99²⁰ = 0.818. The probability that at least one is slow is 18.2%. Fifty calls: 39.5%. A hundred: 63.4%.

This is only correct under one assumption: that the twenty calls fail independently. That assumption is where the math diverges from reality, and it's worth being precise about when it holds and when it doesn't.

There are three structurally distinct causes of tail latency.


Case 1: Local Queueing and Scheduling Jitter

A request arrives at a replica and lands behind something expensive — a long-running query, a stop-the-world GC pause, a thread scheduled to a throttled core. The request isn't computationally expensive. It's waiting. The replica is the unit of contention, and the slowness is local to it.

This is the case where the independence assumption holds. Different replicas have different queues. The probability that a second request sent to a different replica lands behind the same expensive operation is low — queue depths across replicas are, in practice, roughly independent.

This is the case hedged requests were designed for.

Hedged requests. Send a request to replica A. If it hasn't responded within a threshold — set at a percentile of the recent observed latency distribution, not a static value — fire a second identical request to replica B. Take whichever response arrives first. Cancel the other.

The threshold choice is non-trivial. A static threshold calibrated on yesterday's P95 will be wrong when load patterns shift or a deployment changes service time characteristics. The threshold needs to track a rolling latency distribution — hedging at a live percentile, not a hard coded constant. Set it too low and you hedge on nearly every request, collapsing the overhead savings. Set it too high and you hedge too late to capture the tail.

The overhead calculation also requires care. If you hedge on 5% of calls, each hedged call sends one additional request. At steady state, overhead is approximately 5%. But you hedge because the service is slow — and when the service is slow, the cancellation that terminates redundant work arrives late, meaning both requests do significant computation before one is cancelled. Overhead during the period when you need hedging most is higher than the steady-state estimate. Size your replica capacity with this in mind.

Tied requests take this further. Rather than hedging after a delay, send the request to two replicas immediately, but include a cancellation token that both replicas carry. When replica A dequeues the request and begins processing, it broadcasts a cancellation bearing the token to replica B. Replica B discards the work if it hasn't started; otherwise it races to completion and the slower result is dropped.

The coordination mechanism matters. Replica A must know how to reach replica B — either through a shared coordination layer that tracks token-to-replica mappings, or through direct replica-to-replica communication if the routing layer exposes it. The latency benefit of tied requests over hedged requests is the elimination of the hedge delay; the cost is the coordination protocol and its failure modes. If the cancellation channel is slow or unreliable, tied requests degrade toward hedged requests with extra overhead.

The hard constraint on both mechanisms: the operation must be idempotent. Duplicate requests must produce the same result as a single request, with no observable side effects. This makes hedging and tied requests natural fits for read-heavy fan-out — search result fetching, recommendation scoring, feature hydration — and categorically wrong for writes unless you've built idempotency keys into the write path.


Case 2: Shared Dependency Slowness

A downstream database is under write pressure and query latency rises across all callers. A cache tier's eviction rate spikes and miss rates rise system-wide. A rate-limited external API starts returning slowly for all tenants simultaneously.

In this case, the independence assumption breaks down. Every replica of the calling service shares the same downstream dependency. Sending a hedged request to a different replica of your service doesn't help — both replicas will be slow for the same reason. You've doubled your request volume and bought nothing, while adding load to a dependency that is already struggling.

This is the case where hedging worsens the situation.

The correct mechanisms here operate on the dependency, not the caller.

Isolation. If a shared dependency is slow for some callers and not others, the dependency is shared when it shouldn't be. Tenants with different load profiles, priority tiers, or SLOs should not share a connection pool, a cache tier, or a database cluster. Isolation converts a global slowdown into a contained one.

Circuit breaking. When a dependency's latency or error rate exceeds a threshold, stop sending it requests. Return a fast failure or a cached result instead. This protects the caller from absorbing the dependency's slowness, and gives the dependency a chance to recover without continued load pressure. The circuit closes again after a probe request succeeds — don't reopen it based on time alone, because the dependency may still be recovering.

Result caching with stale-while-revalidate semantics. If the dependency's data can tolerate bounded staleness, serve the last known good result while revalidating asynchronously. When the dependency recovers, the cache warms and fresh results resume. The SLO implication is explicit: you're trading consistency for availability, and the bound on staleness needs to be a deliberate product decision, not an accident of cache TTL configuration.

Read replicas and cross-region routing. If the shared dependency is a database under write pressure, read replicas absorb read traffic without contending with the write path. If the dependency is regional and the slowness is region-specific, routing reads to a different region's replica is structurally equivalent to hedging — except it addresses the actual source of the problem rather than routing around it at the wrong layer.

The diagnostic question for Case 2: is the latency percentile of the upstream service correlated across replicas? If P99 rises on replica A and replica B simultaneously, the cause is shared. Hedging will not help. Look at what those replicas have in common.


Case 3: Coordinated Infrastructure Failure

A network segment between availability zones develops elevated latency. A rack loses partial power and servers on it run at reduced clock speeds. A noisy neighbor on shared physical hardware saturates the memory bus, slowing all VMs on that host. A BGP route change adds 40ms to cross-datacenter traffic for a subset of traffic flows.

This case is structurally similar to Case 2 — the independence assumption breaks — but the cause is infrastructure rather than application-level dependency. The distinction matters because the remediation operates at a different layer.

Hedging helps here only if the second request is routed to infrastructure outside the affected domain. If replica A is on rack 3 and replica B is also on rack 3, hedging between them during a rack-level power event accomplishes nothing. Hedging to a replica on rack 7 — or in a different availability zone — routes around the problem at the cost of cross-zone latency, which may itself be elevated if the network segment is the source of the issue.

This requires topology-aware routing, not just replica selection. The routing layer must know which replicas share physical infrastructure, and must be capable of preferring replicas that are topologically distant from a known slow replica. Most service meshes expose this through locality-aware load balancing — requests prefer same-zone replicas for latency, but can be configured to route away from zones that are reporting elevated error rates or latency.

The diagnostic question for Case 3: does the latency elevation affect a subset of your replicas, and do those replicas share physical infrastructure? Check host placement, rack assignment, and availability zone. If the slow replicas form a physical cluster, the cause is infrastructure. Hedging helps only if your routing layer can guarantee topological separation between the original request and the hedge.


SLO Derivation

Most teams derive per-service P99 from load tests and set it as the SLO. This is the wrong direction. The useful derivation runs the other way: given a page-level latency SLO and a fan-out depth, what per-service latency target do you need?

If you need page-level P99 of 100ms with N parallel calls, and if the calls are independent (Case 1), the per-service target P is:

P = 1 - (0.01)^(1/N)

For N=20: P = 1 - 0.01^(0.05) ≈ 0.9989. You need each service to achieve P99.9 at 100ms, not P99. For N=50: P99.97. For N=100: P99.98.

These are the numbers that should appear in your service-level SLOs — derived from the composition your system actually performs, not from per-service load tests in isolation.

If the calls are not independent — Cases 2 and 3 — this formula doesn't apply. Correlated failures mean the effective fan-out for the purposes of tail latency is lower, but the ceiling is a shared dependency or infrastructure domain, not a per-service characteristic. In this case, the SLO needs to be set on the shared dependency, not on the callers.


Diagnosis Before Mechanism

The reason these three cases matter is that the mechanisms are not interchangeable. Applied to the wrong case, they either fail silently or make things worse.

Cause Hedging Circuit Breaking Isolation Topology-Aware Routing
Local queueing jitter ✓ fixes it no effect no effect helps if combined with hedging
Shared dependency slowness makes it worse ✓ fixes it ✓ fixes it structurally no effect
Infrastructure failure helps only with topological separation no effect no effect ✓ fixes it

Before reaching for a mechanism, answer three questions:

  1. Is the latency elevation correlated across replicas of the slow service, or is it local to specific instances?
  2. If correlated — do the slow replicas share a downstream dependency, or do they share physical infrastructure?
  3. Does your routing layer have visibility into physical topology sufficient to guarantee topological separation when hedging?

The answers determine which case you're in. The case determines the mechanism. Hedging is not a general solution to tail latency. It is a precise solution to one specific cause of it — and a liability when applied to the other two.

Read more