SysDesignPrep.com
Interactive concepts

Tail latency and hedged requests: an interactive fan-out simulator

Tail latency is how long the slowest requests take: the p99, the p99.9. It matters more than the average because of fan-out. When one user request calls many backends in parallel and waits for all of them, a slowdown that hits 1 call in 100 hits most user requests. Hedged requests, sending a second copy of a call that is running late and taking whichever answers first, cut that tail for a few percent more load.

In the playground, a backend answers in about 10 ms but stalls for 200 ms to a second 1% of the time. Change how many backends each user request calls, and when to hedge.

Backends per requestHedge

Each user request calls 100 backends in parallel and waits for the slowest. Every backend still stalls only 1% of the time, but the chance that at least one of 100 does is 63%: 63% of user requests waited on a stall. The backends’ p99 is 29 ms; the user’s median is 436 ms and p99 993 ms.

Requests that wait on a stalled backend
0%50%100%0100200backends per request
User request latency at each percentile
p50p90p99p99.910 ms100 ms1 s
Median request436 msone backend alone: about 10 ms
p99 request993 msbackend p99: 29 ms
Waited on a stall63%of user requests
Backend calls100%of the calls without hedging
1 − 0.99ⁿ, without hedgingthis run4,000 user requests; each backend call stalls 1% of the time.

Why fan-out makes the tail the common case

If each backend is slow with probability p, a request that calls n of them and waits for all is slow with probability 1 − (1 − p)n. With p = 1%, that is 10% for 10 backends and 63% for 100. In the playground, every backend’s p99 is under 30 ms, yet with 100 backends per request the median user request takes 436 ms. The backends’ rare tail has become the user’s typical experience. Search, feeds, recommendation and any page assembled from many services all fan out like this.

Backends per requestRequests that hit a 1% stall
11%
1010%
5040%
10063%
50099%

Where the tail comes from

Slow calls are usually not slow servers but slow moments: a garbage-collection pause, a request queued behind a large one, a cache miss to disk, a noisy neighbour on the same machine, a background compaction, a network retransmit. They are short and random, which is exactly why retrying elsewhere works: the replica you ask second is unlikely to be in the same moment.

Hedged requests

Send the call to one replica. If it has not answered by about the 95th percentile of its normal latency, send the same call to another replica, use whichever answers first and cancel the other. Only the slowest few percent of calls get a second copy, so the extra load is small: hedging after 20 ms adds 2% more calls in the playground and brings the median request with 100 backends from 436 ms to 29 ms. Hedge too early and you double the load instead, as the last preset does after 5 ms, and the extra load makes every server slower.

The idea comes from Jeffrey Dean and Luiz Barroso’s 2013 paper The Tail at Scale, which also describes tied requests: send both copies at once, each telling its server about the other, so whichever starts first cancels its twin. Hedge only calls that are safe to run twice, reads or idempotent writes.

Other ways to cut the tail

  • Fan out less: fewer, larger partitions, or answering from a cache or precomputed result.
  • Do not wait for everyone: return results from the backends that answered within a deadline, if a slightly incomplete answer is acceptable, as search often is.
  • Route around slow servers: least connections or power of two choices avoid sending work to a server that is already behind.
  • Remove the cause: smaller GC pauses, separate queues for big and small requests, isolating background work.

A hedged request in code

// hedged request: if the first replica is slow, ask a second and take whichever answers first
func hedged(ctx context.Context, replicas []Client, req Request, hedgeAfter time.Duration) (Response, error) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()                                  // the loser is cancelled when we return
    results := make(chan result, 2)
    send := func(c Client) { r, err := c.Do(ctx, req); results <- result{r, err} }

    go send(replicas[0])
    select {
    case res := <-results:
        return res.resp, res.err                     // answered in time: no hedge needed
    case <-time.After(hedgeAfter):                   // about the p95 of this call
        go send(replicas[1])
    }
    res := <-results                                 // first of the two to answer
    return res.resp, res.err
}

In a system design interview

When a request fans out, do the arithmetic out loud: “The search page queries 100 index shards. If each is slow 1% of the time, 63% of searches would wait on a slow shard, so the shards’ p99 is our median. Each shard call is hedged to a second replica after its p95, about 20 ms, which costs a few percent more load, and the page returns whatever shards answered within 200 ms.” Expect the follow-ups: what the hedge does to load during an incident, which calls are safe to duplicate, and how you measure p99 and p99.9.

Read the guide to tail latency, see queueing and Little’s law for why busy servers have long tails, or watch a strong candidate design typeahead search.

Questions people ask

What is tail latency?

The latency of the slowest requests, usually measured as the 99th or 99.9th percentile. It matters because users notice the slow requests, and because in systems where one request calls many services, the slowest of those calls decides how long the whole request takes.

Why does fan-out make tail latency worse?

A request that calls n backends in parallel and waits for all of them is slow if any one is slow. If each is slow 1% of the time, the chance at least one of n is slow is 1 − 0.99ⁿ: about 10% for 10 backends and 63% for 100. The backends’ p99 becomes the request’s median.

What is a hedged request?

A duplicate of a call that is taking longer than usual, sent to another replica after a short delay, typically around the call’s 95th percentile latency. Whichever copy answers first is used and the other is cancelled. Because only the slowest few percent of calls are duplicated, the extra load is small.

When should a hedged request be sent?

At about the 95th percentile of the call’s normal latency. Earlier, and most calls get a second copy, which nearly doubles backend load and slows everything; later, and the hedge arrives too late to help much. The delay is often set adaptively from recent latency measurements.

What is the difference between hedged and tied requests?

A hedged request sends the second copy only after a delay. A tied request sends both copies at once, each carrying the identity of the other server, and the first server to start working on it tells the other to cancel. Tied requests cut latency further at the cost of a little extra messaging. Both come from Google’s 2013 paper The Tail at Scale.

What causes tail latency?

Usually short, random slowdowns rather than slow machines: garbage-collection pauses, queueing behind a large request, cache misses that go to disk, background work like compaction, contention with other tenants on the same host, and network retransmits. Their randomness is what makes retrying on another replica effective.

How else can you reduce tail latency?

Fan out to fewer backends, return partial results after a deadline when that is acceptable, route requests away from busy servers with least-connections or power-of-two-choices balancing, keep separate queues for small and large requests, and reduce the causes, such as long garbage-collection pauses and background work on serving machines.