A failing job retried itself into taking down the service it depended on: how should retries actually be designed?
An upstream API started returning errors. My worker retried, as designed. Within a few minutes the queue had thousands of jobs all retrying, the upstream got substantially more traffic than normal, and what had been a partial outage became a complete one.
When the upstream recovered it immediately fell over again, because the entire backlog hit it at once.
My retry logic is a fixed three attempts, five seconds apart. That was clearly wrong and I do not know what right looks like.
How do people design this so that a failure stays contained instead of amplifying?
@circuit_breaker_cem · 3w ago · 4 replies
The piece that would have prevented the amplification specifically: stop calling something that is clearly down.
A circuit breaker is simple in concept. Count recent failures for a dependency; if they cross a threshold, stop making calls entirely for a period and fail immediately. After the period, let one request through - if it succeeds, resume; if not, wait again.
What that does for your incident:
Your service stops adding load to something already struggling. This is the difference between a partial outage and the total one you got.
Jobs fail fast instead of occupying workers for the full timeout. A large part of these incidents is every worker blocked waiting on a dead dependency, so nothing else moves either.
Recovery is controlled. One probe request rather than the whole backlog.
Alongside it, two things worth having:
A concurrency limit per dependency. Never more than N in flight to any one external service, regardless of how many jobs are ready. This alone caps the damage.
Rate limiting on the way out, so a drained backlog leaves at a survivable pace rather than all at once.
Most queue libraries have some of this built in, and it is usually off by default.
Reply
Report
@thundering_herd · 3w ago · 3 replies
The recovery half of your incident deserves its own name, because the fix for it is separate from everything above. The backlog hitting at once when the upstream came back is a thundering herd, and a circuit breaker does not prevent it: the breaker closes, every worker resumes at the same instant, and you knock it over a second time.
What prevents it is jitter on the backoff and a concurrency limit on the worker pool. Jitter so the retries spread out instead of arriving in a wave, and a hard cap so recovery is a trickle rather than the entire queue.
Reply
Report
@circuit_breaker_cem · 3w ago
Correct, and the half-open state is the part people implement as a boolean and then get exactly this. One request through, not all of them.
Reply
Report
@oncall_last_night · 3w ago
Learned this at 3am. The second outage was worse than the first because everything was warm and confident.
Reply
Report