👋 Hey {{first_name|there}},
The Pain You've Felt
You know this story.
Your payment system hiccups. Maybe two seconds of slowness. Perhaps a brief network flutter. Nothing apocalyptic. Then checkout begins crumbling. Queues balloon. Everything feels sluggish, and somehow the damage spreads far beyond the initial problem.
I used to blame unreliable payment providers. Sometimes they deserve it. But here's the uncomfortable truth that took me years to accept: we transform minor upstream issues into catastrophic failures, and we do it with the very mechanism designed to save us, retries.
The typical failure mode looks innocent at first. Default retry behavior seems reasonable. No jitter? No problem, we think. Multiple layers retrying simultaneously? Sure, redundancy is good. Infinite attempts? Well, persistence pays off, right? Wrong. Dead wrong. One timeout spawns a flood of desperate requests, your own services become the distributed denial-of-service attacker you feared, and suddenly, rate limits trigger, thread pools saturate, and the entire checkout flow collapses like a house of cards in a windstorm.
Treat retries as a production safety feature requiring strict governance, not a convenience you sprinkle everywhere like salt.
🧭 Mindset shift
From: "Retries improve reliability, so add them whenever something fails."
To: "Retries generate load. Budget them carefully, jitter them religiously, and cut them off decisively."
Retries can be good. Absolutely. I'm not anti-retry. I'm anti-unconscious retry.
Consider the mathematics of disaster. If every timeout spawns three additional requests, and those requests also timeout because the dependency is already struggling, you're actively increasing traffic precisely when the system can least handle it. That's the death spiral. Circuit breakers exist to sever that spiral before it strangles your infrastructure. Jitter exists to prevent synchronized thundering herds, where thousands of clients retry at identical milliseconds, creating devastating traffic spikes that no system can absorb.
Two rules that keep you honest:
Only one layer owns retries for any given external call. Everyone else either fails fast or propagates errors upstream without heroics.
Retry budgets are part of your SLO budget, and if you spend them freely during peacetime, you'll burn through your entire error budget when the next incident strikes and you need that cushion most.
🧰 Tool of the week: Your Retry Storm Guardrail Standard
Make this mandatory. Encode it in shared libraries. If teams need to deviate, they document their reasoning in writing.
Define retry ownership clearly.
Choose exactly one layer that retries for each outbound dependency. Usually, the calling service owns this. Gateways shouldn't retry. SDKs shouldn't retry. Pick one owner.
Set hard attempt limits.
Default to two total attempts: one initial call plus one retry. Increase only with evidence and measurement. Never, under any circumstances, allow infinite retries.
Use exponential backoff with jitter every single time.
Backoff duration grows with each attempt, giving struggling systems breathing room. Add jitter so clients spread their retries across time instead of creating synchronized waves that crash against your infrastructure like battering rams.
Retry only on genuinely safe failures.
Retry transient network errors, timeouts, and explicit retryable signals from downstream services. Do not retry validation errors, authentication failures, or business-level denials. Do not retry non-idempotent writes unless you've implemented idempotency keys that guarantee safe replay.
Add per-call timeouts smaller than caller timeouts.
Inner calls must timeout earlier than the overall request, or you'll stack waiting periods inside waiting periods, creating timeouts within timeouts like Russian nesting dolls of dysfunction.
Circuit breaker per dependency.
When failure rates cross a threshold within a short window, trip the breaker open. While open, fail fast instead of attempting doomed requests. After a cooldown period, transition to half-open with limited probes to test recovery.
Bulkhead each dependency.
Separate thread pools, connection pools, or concurrency limiters isolate dependencies from each other. One failing dependency shouldn't consume all available workers and strangle healthy operations.
Return degraded responses when possible.
Define fallbacks for non-critical features. For checkout, this might mean displaying a "payment temporarily unavailable" page instead of letting users watch spinners until they abandon their carts.
Expose comprehensive telemetry.
Metrics matter. Track retries attempted, retry success rates, circuit breaker state, breaker trip events, bulkhead saturation, and timeout counts. Alert when breakers stay open too long or when retry volumes spike unexpectedly.
Have a kill switch ready.
A configuration flag that disables retries or forces breakers open for specific dependencies gives you manual control when automation makes the wrong call during a crisis.
🔍 Real World Example: Payment Timeout Cascade
Scope:
Checkout calls Payment Authorization. A minor latency spike triggers a chain reaction.
Context:
The checkout service uses a three-second request timeout, and it calls the payment service with a 2.8-second timeout, leaving barely any margin for error. Both the HTTP client and the service code retry independently. No jitter exists. No circuit breaker protects the flow. Under load, timeouts multiply like rabbits.
Applying the standard step-by-step:
Retry ownership becomes explicit: only Checkout retries payment calls. HTTP client retries? Disabled. Gateway retries? Disabled. One owner.
The attempt limit drops to two total attempts.
Backoff with jitter adds a small randomized delay before the first retry instead of hammering immediately.
Safe failures only. Retry on timeouts and connection errors. If payment returns a clear decline code, accept it without retry.
Timeout ladder gets fixed: payment call timeout becomes 800 milliseconds, not 2.8 seconds, leaving Checkout room for one retry plus page rendering.
Circuit breaker monitors payment timeouts, and when they exceed the threshold within a rolling window, the breaker opens, and Checkout fails fast instead of piling on.
Bulkhead ensures payment calls use a dedicated concurrency limiter, preventing them from starving other critical work.
The degradation path activates when the breaker opens, showing users a clear "payment unavailable" message instead of queuing work destined to fail.
Telemetry reveals the story: retries spike first as the problem emerges, then the breaker opens as thresholds cross, then checkout p95 latency stabilizes instead of climbing toward infinity.
Kill switch empowers on-call engineers to force the breaker open during a provider incident, protecting the rest of checkout from collateral damage.
What success looks like:
Payment experiences a rough five minutes. Checkout doesn't melt down. Error rates increase, yes, but in controlled and explainable ways that preserve core functionality. When payment recovers, the circuit breaker transitions to half-open and traffic resumes gradually instead of as a sudden overwhelming flood that could trigger a second failure.
Small confession:
I sometimes configure breakers to trip slightly too early. It looks aggressive. Some engineers complain. But here's what I've learned: it's far easier to relax an overly sensitive circuit breaker than to recover from a retry storm that's already destroyed your infrastructure.
✅ Do this / avoid this
Do:
Make retry ownership explicit with one designated layer.
Cap attempts with a default of two total.
Use exponential backoff with jitter every single time.
Add circuit breakers and bulkheads per dependency.
Fail fast and degrade gracefully instead of queuing hopeless work.
Monitor retries and breaker state, not just error rates.
Avoid:
Infinite retries or automatic retries on every exception type.
No jitter, which creates synchronized retry waves.
Retrying non-idempotent writes without idempotency keys.
Letting every layer retry: client, gateway, service, and SDK all retrying independently.
Timeouts that leave no room for retries, rendering, or fallback logic.
Solving incidents by increasing timeouts first, instead of understanding root causes.
🧪 Mini Challenge: Stop One Storm in 45 Minutes
Goal: Eliminate one retry storm risk quickly.
Pick one dependency in a critical flow. Payments? Inventory? Authentication?
Identify how many layers retry today. Reduce it to one.
Set attempts to two total and add exponential backoff with jitter.
Add a circuit breaker with short cooldown and half-open probing.
Add a bulkhead limiter for that dependency.
Add two metric tiles: retries per minute and breaker state over time.
Hit reply. Tell me which dependency you hardened.
🎯 Action Steps for This Week
Adopt a mandatory retry and breaker library standard across all services.
Audit your top five dependencies. Remove multi-layer retries.
Require idempotency keys for any write path that retries.
Add a dependency health section to your main dashboard showing breaker state, retries, timeouts, and saturation.
Establish a policy: no new outbound call ships without a retry budget, a circuit breaker, and comprehensive telemetry.
By week's end, aim to have checkout and one other core flow protected by the standard, with retries capped and breakers visible.
👋 Wrapping up
Retries are load. Budget them.
Jitter prevents synchronized waves. Use it.
Circuit breakers stop death spirals. Install them.
Bulkheads protect healthy systems from sick dependencies. Isolate everything.
⭐ Most read issues (good place to start)
If you’re new here, these are the five issues readers keep coming back to:
One quick question: Which dependency causes the most cascading failures for you today?
Hit reply. Tell me in one sentence.
Thanks for reading.
See you next week,
Bogdan Colța
Tech Architect Insights