Skip to content

    AI Architect Case Files

    Case: Retry Storm After a Rate Limit, Fixed With Jitter

    A telecom's call-summary workers hit a tokens-per-minute limit, retried in lockstep and starved live traffic. A token bucket and jittered backoff fixed it.

    By Published 7 min read

    • ai-architect
    • case-file
    • industry-telecom
    • pattern-backoff-jitter
    • failure-retry-storm
    • kind-composite
    Share

    Composite scenario, not a real company. Assembled from common industry patterns; every number is illustrative and its assumptions are listed at the end.

    This case belongs to Reliability and scaling for AI systems.

    NOTE

    The billing dispute case is about retries multiplying down three layers. This case has one layer. The damage comes from timing, from ignoring the provider's wait hint, and from no tokens-per-minute headroom.

    Context

    • After each customer call, a worker asks a model to summarise the transcript into a care note.
    • 200 workers share one provider account. The rate limits are set per organisation and shared with other teams.
    • At 02:00 a scheduled backfill re-summarises 6,000 older calls for a new analytics field.
    • Each task sends 2,500 uncached input tokens and receives 300 output tokens.

    Problem statement

    The provider measures requests per minute (RPM), input tokens per minute (ITPM) and output tokens per minute (OTPM). The tightest limit sets the pace.

    Limit Value Per task Tasks per minute
    ITPM 400,000 2,500 160
    OTPM 80,000 300 266.7
    RPM 1,000 1 1,000

    ITPM binds: 160 tasks per minute, 2.67 per second. Live summaries use 72 per minute, so headroom is 88 per minute, 1.47 per second.

    At 02:00:00 all 200 workers send a request in the same second. The 429 responses return, and every worker retries after a fixed 1 s, ignoring the wait hint, up to 5 attempts. At most 1.47 x 5 = 7.3 tasks finish out of 1,000 attempts: over 99% of attempts are rejected, about 137 attempts per completed task, and live summaries stall.

    Constraints

    • The provider's limits are fixed, and ITPM is estimated at the start of each call, so the real use can differ slightly.
    • Live summaries must keep flowing during the backfill.
    • Results are written to a care-note store, so a repeated task must not create two notes.

    Why the traditional approach fell short

    Agent-typed notes are slow and uneven, and templates capture fields, not the story of the call. The batch scripts that did the old backfills used a fixed retry delay, which is harmless for one client and harmful for 200 started by one clock. A generic retry library with defaults has no idea what the provider's limit is.

    Options considered

    Option Why accepted or rejected
    A. Non-AI scheduling: start the backfill with one worker, in the quiet hours Kept as the stopgap. It removes the storm, but the next job with 200 workers brings it back
    B. Ask for a higher limit Rejected as the only fix: more headroom helps, but synchronised retries still waste calls
    C. Jittered backoff alone Rejected alone: 200 clients and 1.47 per second of headroom need a window of 136 s
    D. Shared token bucket in the Model gateway, queue for waiting tasks, retry-after honoured with jitter, one retry owner Accepted
    E. The provider's batch interface for the backfill Worth doing for any job that can wait, since the rate-limit page lists separate limits for it. Live traffic still needs D

    Solution architecture

    Figure 1. The gateway paces calls and owns every retry

    flowchart LR accTitle: Gateway with a token bucket and one retry owner accDescr: Workers and the Orchestrator send calls to the Model gateway. The gateway takes a token from a shared bucket before calling the LLM provider. On a 429 it waits the provider hint plus a random extra and retries within a budget. When the budget is spent, the task goes back to the Event bus with a delay. Orch["Orchestrator: live summaries"] --> MG EB["Event bus: summary tasks"] --> Wk["Worker x 200"] Wk -->|"one call per task, no retry"| MG["Model gateway: token bucket, sole retry owner"] MG -->|"token available"| LP["LLM provider"] LP -->|"429 with retry-after"| MG MG -.->|"retry budget spent: requeue with delay"| EB MG --> TS["Trace store"]

    How to read this: only the gateway retries, and tasks that cannot be served wait in the queue instead of hammering the provider.

    Figure 2. One task that meets a 429

    sequenceDiagram accTitle: Task with a 429 and jittered retry accDescr: The Worker sends a task to the Model gateway. The gateway waits for a bucket token, calls the provider and receives a 429 with a retry-after value. It waits at least that long plus a random extra, retries once if the retry budget allows, and otherwise returns the task to the Event bus with a delay. Every attempt is traced. participant W as Worker participant M as Model gateway participant L as LLM provider participant E as Event bus participant T as Trace store W->>M: summarise task 4711 Note over M: Take one token from the bucket or wait M->>L: request alt accepted L-->>M: summary M-->>W: summary else 429 with retry-after L-->>M: 429 and retry-after seconds Note over M: Wait retry-after plus a random extra opt retry budget allows M->>L: retry once L-->>M: summary M-->>W: summary end opt still no summary M-->>W: not done, retry later W->>E: requeue task 4711 with delay end end M-)T: attempts and outcome

    How to read this: the first alt separates success from a 429, and the opt blocks are the two ways a rejected call ends.

    How AI solved it, step by step

    One summary task during the 02:00 backfill (interactive when JavaScript is on)

    As designed

    1. Take a task. A Worker takes task 4711 from the Event bus and sends one call to the Model gateway. The Worker does not retry.
    2. Wait for a token. Decision: the gateway's shared bucket admits 2 tasks per second, 75% of the provider's limit. Live summaries go first, so the backfill gets the remaining 0.8 per second.
    3. Call the provider. The gateway sends the request. Most calls succeed and nothing else happens.
    4. Read the 429. If the provider still replies 429, the gateway reads the retry-after value. Waiting less would fail, so that is the minimum wait.
    5. Add jitter. The gateway adds a random extra between 0 and 2 s for the first retry, so 200 workers do not return in the same second.
    6. Retry once or requeue. If the retry budget allows, the gateway retries once. Otherwise the task goes back to the queue with a delay and the trace records the attempts.

    Without the fix

    1. 200 calls in one second. The 02:00 job starts all workers together and sends 500,000 tokens of input in one second.
    2. Fixed 1 s retry. Every rejected worker waits exactly 1 s, ignores retry-after and sends again.
    3. Wave after wave. Each wave offers 200 calls against about 1.47 per second of headroom. Live summaries queue behind them.
    4. Give up after 5. About 7 tasks finish. The rest fail and return to the queue, where the next worker picks them up and the storm repeats.

    In the starting numbers, the burst of 25 at second 1 admits 10 and holds 15 back. Raise the capacity to 25 and watch the held count at second 1 fall to 0.

    Gateway bucket: 2 tasks per second, burst of 10 (interactive when JavaScript is on)
    Second 00 arrived, 0 accepted, 0 rejected, 10 tokens left
    Second 125 arrived, 10 accepted, 15 rejected, 0 tokens left
    Second 22 arrived, 2 accepted, 0 rejected, 0 tokens left
    Second 32 arrived, 2 accepted, 0 rejected, 0 tokens left
    Second 42 arrived, 2 accepted, 0 rejected, 0 tokens left
    Second 50 arrived, 0 accepted, 0 rejected, 2 tokens left
    Second 60 arrived, 0 accepted, 0 rejected, 4 tokens left
    Second 78 arrived, 6 accepted, 2 rejected, 0 tokens left
    Second 80 arrived, 0 accepted, 0 rejected, 2 tokens left
    Second 90 arrived, 0 accepted, 0 rejected, 4 tokens left

    Under the hood

    Technique What the primary source says Design choice here
    Backoff alone In AWS's simulation it "helps a small amount, but doesn't solve the problem". The fix is to "add jitter" Never use fixed or purely exponential delays for a fleet
    Jitter Randomness "to spread the retries around in time", because clients that back off to the same time overload again Random extra on top of retry-after
    Local token bucket AWS limits retries "locally using a token bucket" One shared bucket in the gateway
    retry-after The provider docs say to wait that many seconds, and earlier retries will fail It is the floor of every wait

    Jitter spreads arrivals but only so far. Spreading 200 retries over a window of W seconds gives 200 / W per second: 100 at W = 2, 25 at 8 and 3.33 at 60, against 1.47 per second of headroom. A window of 136 s would be needed, so the bucket and the queue do the pacing and jitter breaks the lockstep. Mechanism: Under the hood: Backoff with jitter.

    What went wrong / failure modes

    Failure Designed mitigation
    A 429 for the monthly spend cap has no retry-after and never clears Detect the spend-limit error code, stop retrying, alert a human
    The SDK's own retries (twice by default) stack on the gateway's Set the client's retry count to 0 behind the gateway
    The backfill ramps from zero to full in a second, which can trip acceleration limits Ramp traffic gradually
    A rate-limit 429 arrives without retry-after Fall back to the jittered exponential schedule
    Task sizes vary, so counting tasks hides token use Meter estimated input tokens instead of tasks
    A task is repeated after a requeue Idempotency key on the care-note write

    Results

    These are illustrative targets, not measurements.

    Measure Fixed retry, no bucket Target design
    Attempts per completed task during the first wave about 137 at most 1.2
    Provider ITPM used at the limiter's maximum uncontrolled 120 x 2,500 = 300,000 (75%)
    Backfill speed stalled 48 per minute, 6,000 tasks in 125 minutes
    Live summaries during the backfill stalled unaffected

    Tradeoffs and when NOT to use this approach

    • The 25% margin is unused capacity. Shrink it only with measurements of the estimation error.
    • A central bucket is a shared component and a possible bottleneck, so run it replicated or keep its logic trivial.
    • With one client and low volume, a plain exponential backoff is enough.
    • Pacing makes the backfill slower. If it must finish sooner, buy more limit instead of spending retries.

    Design rules extracted

    1. Size sustained load below the binding limit and keep the difference as retry headroom.
    2. Meter calls on the dimension that binds, usually input tokens per minute.
    3. Wait at least the provider's hint, then add a random extra.
    4. One component owns retries; everything else fails fast.
    5. Tasks that cannot be served wait in a queue, not in a retry loop.

    Assumptions

    Last verified: 2026-10-22

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments