API Design

Production at Scale · Simulator 05

Retry amplification storm

When a service starts failing, clients retry — and those retries generate more load on an already struggling service, causing more failures, which cause more retries. Drag fail rate and max retries together and watch the traffic multiplier explode. This is the mechanism behind the "thundering herd on failure" that takes down services during incidents.

Math companion to Retries & backoff and Circuit breaker — interactive amplification only; full contract and failure paths live on those lessons.

InteractiveMath companionModels rel-05
💡 How to Read the Simulator

Bars: baseline vs amplified QPS. Dashed line = system capacity. Curve = multiplier across all fail rates for the chosen retry count, with the current point marked (●).

By the numbers: retry storm math

If every request has an independent failure probability f, and a client retries up to R more times on failure, the expected number of attempts per original request is the truncated geometric series:

# Probability of still failing after k attempts = f^k
# Expected attempts = sum_{k=0}^{R} f^k  (geometric series)

attempts     = (1 − f^(R+1)) / (1 − f)      # when f ≠ 1
             = R + 1                          # when f = 1 (all attempts fail)

amplifiedQPS = baseQPS × attempts
extraLoad    = amplifiedQPS − baseQPS

# The vicious cycle: amplified load → higher f → more retries → …
# You need: amplifiedQPS ≤ capacity at ALL points in the cycle

At low fail rates the series converges quickly — 5% failure with 3 retries gives ≈1.05× amplification. But as f → 1, amplification approaches R + 1. The danger zone is the middle: a 50% outage with 3 retries gives 1.875× ((1 − 0.5⁴) / 0.5 = 1.875), potentially pushing an overloaded system past capacity and making the failure rate worse.

Retry Amplification worked trace

Failure Rate (f) Max Retries (R) Attempts Multiplier Amplified Load (QPS) Status vs 1M/s Capacity
10% 3 1.111× 555,500/s (base 500k) Allowed (Normal operation)
50% 3 1.875× 937,500/s (base 500k) Allowed (Near capacity)
80% 3 2.952× 1,476,000/s (base 500k) OVERLOAD (Cascade Failure)
100% (Outage) 3 4.000× 2,000,000/s (base 500k) OVERLOAD (Total System Collapse)
✅ Try this

1. Set base QPS to 500 k/s, capacity 1 M/s, fail rate 50%, retries 3 → attempts = 1.875× → amplified = 937.5 k/s, still under capacity. 2. Raise fail rate to 80% (keep R=3) → attempts ≈ 2.95× → amplified ≈ 1.48 M/s → over capacity → real fail rate rises → cascade. 3. Set retries to 0 → no amplification, fail rate has no multiplier effect. 4. Keep retries at 2 and sweep fail rate from 10% to 90% — watch the curve vs the capacity line. (At f=50%, R=5 is only ≈1.97× → ~984 k/s — still under 1 M; the storm needs high f or higher R, not R alone.)

⚠️ Modeled, not measured

This is a first-principles model using the geometric series for independent retry attempts. Real retry storms are more complex: retries may be correlated, exponential backoff with jitter changes timing, circuit breakers may open before all retries fire, and retry budgets cap total attempts. The model shows the structural amplification — treat numbers as illustrative and use them to build intuition for retry budget sizing.

Under the hood: how retry storms actually work

A retry storm is a positive feedback loop of demand amplification. When a downstream dependency experiences transient latency or packet loss, the client-side execution path amplifies the incident:

  1. Initial Timeout: The client triggers a request. If the server does not respond within the client's timeout window (e.g. 500ms), the client aborts the connection.
  2. Re-issue Request: The client immediately issues another request to retry:
    for (int i = 0; i <= MAX_RETRIES; i++) {
      try {
        response = client.send(request);
        break;
      } catch (TimeoutException e) {
        sleep(backoff(i)); // if backoff is 0, retries fire instantly!
      }
    }
  3. Retry Amplification: As failures increase, the volume of retries quickly consumes the thread pools, memory, and database connections of the downstream service, turning a minor issue into a major outage.

How to debug & inspect it

To identify retry storms, monitor the ratio of incoming requests to unique customer sessions or trace duplicate request IDs in system logs.

$ grep "X-Request-ID: req_abc123" /var/log/nginx/access.log 172.16.0.4 - [01/Jul/2026:20:00:01 +0000] "GET /v1/data HTTP/1.1" 503 543 "req_abc123" 172.16.0.4 - [01/Jul/2026:20:00:02 +0000] "GET /v1/data HTTP/1.1" 503 543 "req_abc123" 172.16.0.4 - [01/Jul/2026:20:00:04 +0000] "GET /v1/data HTTP/1.1" 503 543 "req_abc123" # Identical request IDs repeating with growing intervals indicate client retries

Symptom → Cause → Fix:

Symptom Likely Cause Fix
Downstream load multiplies by 3× during database lock Clients are configured with high max-retries and no retry budgets Implement a retry budget (e.g., maximum 10% of total traffic can be retries)
Traffic spikes occur at exact 1-second intervals after an outage Clients are retrying on a fixed schedule without randomized jitter Implement exponential backoff with full jitter to spread retries out in time
Service remains in an overloaded state long after the bug is fixed Self-perpetuating retry cycles keeping thread pools permanently saturated Deploy a circuit breaker to fail fast and cut off retries at the source

Math companion — this page trains intuition with numbers. Full design contracts live in rel-05-retries-backoff.html.

When not to stop at the simulator

Why the numbers matter

If you cannot re-derive the formulas on this page from first principles (arrivals, service time, capacity, amplification), you do not own the control. Recompute by hand once, then change one input and predict the chart.

Failure translation

Map each sim control to an ops action: raise limit → client 429; add cache hit ratio → DB QPS falls; add jitter → retry peak flattens; add workers → lag falls until lock or DB saturates.

Sources & further reading