Breaker Outside, Retry Inside
Retry and circuit breaker in the wrong order silently breaks both. Both patterns guard the same call to a flaky dependency, but they do opposite jobs — and which one you wrap on the outside decides whether an outage costs you milliseconds or takes down your latency budget. Most libraries let you nest them either way, and the wrong nesting looks fine in every green test.
Two patterns, one call
A retry assumes failures are transient: the timeout blipped, the pod was mid-restart, the connection reset. It tries again — ideally a few times, with backoff and jitter — expecting the next attempt to succeed.
A circuit breaker assumes the opposite. After a threshold of recent failures it trips open and, for a cooldown window, rejects calls instantly instead of letting them hit a dependency that is already on the floor. It's a state machine — Closed, Open, Half-Open — that stops you from hammering a service while it recovers, and stops one slow dependency from tying up every thread in your app.
They pull in different directions on purpose. Retry says "try harder." The breaker says "stop trying." That's exactly why order matters.
The rule: breaker(retry(call))
Wrap the breaker on the outside and the retry on the inside:
breaker(
retry(
call ) )
Now the breaker sees each call as a single unit of work. Retry burns its attempts inside; the breaker records one outcome — success or final failure. Once the breaker is Open, the entire retry sequence is skipped and the call fails in microseconds.
Flip it — retry(breaker(call)) — and every retry attempt pokes the breaker independently. A single logical request now logs three or five failures, so the breaker trips far too early on inflated counts. Worse, when it is Open, your retry loop keeps calling into it, spinning through its backoff schedule against a breaker that was designed to make you stop.
What the wrong order costs
breaker(retry(call)) — correct | retry(breaker(call)) — inverted | |
|---|---|---|
| Failures the breaker counts per request | 1 | up to N (one per attempt) |
| When breaker is Open | whole retry skipped, fails fast | retry loop still spins over backoff |
| Trip threshold | tuned to real request outcomes | trips early on inflated counts |
| Latency during an outage | microseconds | full retry budget wasted |
Resilience4j is the classic trap here: its default aspect order is Retry(CircuitBreaker(...)) — retry on the outside — which is the inverted arrangement, and it's a documented source of inflated failure counts. If you use it, set circuitBreakerAspectOrder above retryAspectOrder so the breaker wraps the retry.
Power moves
- Make retry breaker-aware. Microsoft's guidance is explicit: retry logic should be sensitive to exceptions the breaker throws and stop when the breaker signals the fault isn't transient. Don't retry a
CircuitBreakerOpen/ call-not-permitted error — that's the breaker telling you to back off, not a transient blip. - Add a per-attempt timeout inside the retry, and a total timeout outside everything. The outer timeout caps the whole retry budget so a caller never waits longer than its SLA; the inner timeout gives each attempt a fresh clock.
- One breaker per dependency, not per method. If a single breaker fronts multiple independent backends (shards, regions), one bad shard trips the circuit for all of them. Scope breakers to the resource that actually fails together.
- Don't retry non-transient status. A
4xx(bad request, auth) will fail identically every time — retrying it just feeds the breaker garbage. Reserve retries for timeouts,5xx, and connection resets.
Resources
- Circuit Breaker pattern — Azure Architecture Center
- Retry pattern — Azure Architecture Center
- Resilience4j — getting started & decorator order
- Resilience4j issue #2383 — default order inflates failure counts
- Circuit breaker strategy — Polly docs
Building resilient AI and backend systems? Yeda AI designs, audits, and ships production services that fail gracefully. This is one tip in the Yeda AI Tips series — short, practical patterns for engineers shipping with AI.