Basis Lab

Answers · The systems around the model

Why do retries make an overloaded service worse?

In shortRetries add extra requests precisely when a service is already too busy, so they turn a small overload into a much larger one and can keep the failure going by themselves.

Because a retry is extra load, and it is generated at the moment the service has least capacity to absorb it. One failed request retried twice is three requests in total, so a service already at or over capacity receives a multiple of the work it was already failing to do.

R=r(1+k)R = r(1+k)

The arithmetic

Let rr be the requests users actually made and kk the number of retries allowed after the first attempt. The offered load is R=r(1+k)R=r(1+k). If a client allows three attempts in total, then k=2k=2 and R=3rR=3r: the service is asked to do three times the work at the exact moment its failure rate is rising. The damage is not linear in the retry count because failure increases the chance of further retries. Suppose the service is at 90 percent of capacity and a tenth of requests begin to fail. The original 100 requests per second become 10 failures per second. If each failure is retried twice, that adds 20 requests per second, taking offered load from 100 to 120, which is 1.2 times the original load and about 1.08 times capacity. Those extra requests make more of the original work late or failed, so more retries fire, and the offered load climbs again. The retry policy has become a load generator with a feedback loop.

Why the obvious fix fails

The natural guess is that retries improve reliability because each attempt has some chance of succeeding. That is true when failures are independent and the service has spare capacity: a retry is then a second coin toss. Under overload the chance of success is not independent of the retry. The failure is caused by the queue, the thread pool, the connection pool, or the database already being full. Adding attempts changes the thing that caused the failure, so the retry does not sample the same probability. It makes the next request more likely to fail, including the requests of users who never retried. This is why a policy that is correct for one request is wrong for a million: the millionth retry is not an independent event, it is part of the load.

FIG 1
layerattempts per callrequests reaching this layer
browser to gateway33
gateway to service39
service to backend327
backend to database381

The number that matters is not the retry count at any one layer but the product. A gateway that retries three times, a service that retries three times, and a backend that retries three times present the database with twenty-seven attempts for one call before the database's own retry policy is considered. If a fourth layer also retries three times, the product is eighty-one. Each layer can truthfully say its own policy is modest. The composition is not modest.

The loop does not need the original cause

Once the retries are feeding the failure, the cycle can continue after the original trigger is gone. Requests fail because load is high; clients retry because requests failed; retries raise load; higher load causes more failures. Remove the bad deployment at 10:00 and the service can still be failing at 11:00, because the loop has its own energy. The only exits are to cut the offered load, by rejecting or shedding it, or to add capacity faster than the loop consumes it. Capacity is usually the slower of the two.

What actually helps

Rejecting surplus work immediately is cheaper than queueing it. A request refused in a millisecond costs almost nothing and lets the caller show a useful error. The same request held for thirty seconds in a queue holds a connection and memory, usually misses its deadline, and is often abandoned. A bounded queue with fast rejection at the boundary keeps useful throughput flat above capacity instead of letting it collapse. On the client side, backoff with a random offset spreads retries out. The random offset is not cosmetic: without it, every client that failed at the same instant retries at the same instant, producing waves. A circuit breaker goes further and stops sending requests after enough consecutive failures, failing locally and letting one test request through occasionally. It is the only common mechanism here that can reduce load to zero.

Where this stops being true

Retries are reasonable when failures are rare, independent of load, and the dependency has spare capacity. They are also reasonable when the operation is idempotent and the retry budget is small, with exponential backoff and jitter, at one layer only. The multiplier is not a law about all retries, it is a law about retries during saturation. If the service is at 20 percent of capacity and a single packet is dropped, two retries cost little. If the service is at 100 percent of capacity and every response is slow, two retries can be the difference between a slow service and a dead one. The assumption that matters is whether the failure is exogenous or self-inflicted by load. When it is self-inflicted, adding attempts adds to the cause.

Worth keeping

  • A retry is extra load at the worst moment; if failures are caused by load, retrying raises the cause.
  • Three attempts at four layers is eighty-one calls at the bottom, even though no layer chose eighty-one.
  • Retries help only when failures are rare and independent of load, and the dependency has spare capacity.

Where this goes next

This question is one step of Running a Service That Stays Up, a course that works through its subject from the beginning. The page that teaches this part of it is How a Small Problem Becomes a Large One, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. Does a server being up mean my service is available?No. A server being up only says a check got an answer; availability is the share of user requests that succeeded, measured where users are.
  2. Why does latency explode when utilization gets close to 100%?Waiting time grows as one over the capacity you have left, so halving the spare capacity doubles the wait and the last few percent of load dominate.
  3. What's the difference between a CDN cache and caching inside my own app?A CDN cache sits near the user and can only hold answers that are identical for everyone, while a cache inside your app can be personal but exists once per process.
  4. What's the difference between LRU and LFU, and which one should I use?LRU drops the item unused for longest; LFU drops the item requested least often. Both guess at future use, and the cheap win is refusing one-off arrivals rather than counting requests.
  5. Why can't a system just stay consistent and available when the network splits?During a split, a cut-off machine can only answer from the data it already holds, so it must either risk being wrong or refuse to answer.
  6. How can I tell if my traffic is bursty or just high volume?Compare the variance of the gaps between arrivals with the square of their average gap: high volume alone does not make traffic bursty.

Read alongside