All chapters in this track
- 01Chapter 1: Why Networking Fundamentals Matter
- 02Chapter 2: The OSI Model and TCP/IP Stack, at a Working Level
- 03Chapter 3: Sockets, Ports, and the Connection Lifecycle
- 04Chapter 4: TCP vs UDP: Choosing the Right Transport
- 05Chapter 5: DNS Resolution, Step by Step
- 06Chapter 6: The HTTP Family: HTTP/1.1, HTTP/2, HTTP/3, and WebSockets
- 07Chapter 7: TLS/SSL and Certificates: The Handshake and Why HTTPS Matters
- 08Chapter 8: Load Balancers, Reverse Proxies, and CDNs
- 09Chapter 9: Latency, Bandwidth, and Congestion Control
- 10Chapter 10: API Protocol Choices: REST vs. gRPC vs. GraphQL, from a Networking Angle
- 11Chapter 11: Common Failure Modes: Timeouts, Connection Resets, Retry Storms, Thundering Herd
- 12Chapter 12: Where Networking Abstractions Leak: NAT, Middleboxes, and Other Surprises
- 13Chapter 13: Full Walkthrough: What Happens When You Type a URL and Press Enter
- 14Chapter 14: Common Networking Interview Questions
- 15Chapter 15: Networking Concepts in System Design and Production Debugging
Chapter 11 of 15
Chapter 11: Common Failure Modes: Timeouts, Connection Resets, Retry Storms, Thundering Herd
Why this matters
These four failure modes account for a disproportionate share of real networking-related production incidents, and — unlike a lot of networking theory — they're directly actionable: each has a specific, learnable cause and a specific, standard mitigation. This chapter is the practical payoff of everything covered so far, applied to keeping systems resilient rather than just understanding how they work when healthy.
Timeouts: the silent, essential safety net
[BEGINNER] A timeout is a deliberate limit on how long to wait for a response before giving up — without one, a single unresponsive dependency can hang a caller indefinitely, since TCP itself has no inherent concept of "this is taking too long" for an application to rely on (a connection can sit technically "open" while producing nothing useful for an arbitrarily long time).
[SENIOR INSIGHT] The subtle, commonly-missed point: there isn't one "timeout" — there are several, each protecting against a different failure, and conflating them (or only setting one) leaves gaps:
| Timeout type | Protects against |
|---|---|
| Connection timeout | The TCP handshake itself (Chapter 3) never completing — the destination is unreachable or extremely slow to accept |
| TLS handshake timeout | The TLS handshake (Chapter 7) hanging after TCP connects but before encryption is established |
| Request/response timeout | A connection is established and working, but the other side never sends a complete response |
| Idle/keep-alive timeout | A connection sits open but unused for too long — protects resources, not correctness |
A service configured with only a single, generic "timeout" often has an implicit, framework-default value for the others that may be far too long (or too short) for its actual needs — worth checking explicitly rather than assuming.
Connection resets: an abrupt, informative failure
[BEGINNER] A connection reset (RST packet in TCP) is a hard, immediate termination of a connection — as opposed to TCP's normal graceful close (Chapter 3's four-way FIN/ACK sequence). A reset can be sent when: a server-side process crashes or is killed abruptly (no chance to close gracefully), a firewall/load balancer actively terminates a connection it considers invalid or timed-out, or an application deliberately aborts a connection.
[IN PRACTICE] A common, confusing production pattern: intermittent connection resets specifically on connections that have been idle for a while, caused by an intermediary (a load balancer, NAT gateway, or firewall) silently closing connections it considers "idle too long" from its own perspective — a value that may not match either endpoint's own configured idle timeout. The fix is usually application-level keep-alive traffic (periodic small messages to keep the connection demonstrably active) tuned to be more aggressive than the most restrictive intermediary's idle timeout in the path — which, frustratingly, often has to be discovered empirically rather than being documented anywhere accessible.
Retry storms: when the fix makes the problem worse
[SENIOR INSIGHT] Retrying a failed request is reasonable, sensible behavior in isolation — but naive retry logic, applied at scale during an actual outage, can turn a partial degradation into a total outage. This is a retry storm:
1. A backend service starts struggling (elevated latency, some request failures)
under normal load — perhaps at 90% capacity, degraded but still partially functional.
2. Clients experiencing failures/timeouts immediately retry.
3. These retries ADD MORE LOAD to the already-struggling service — now handling both
the original traffic AND a wave of retries.
4. The added load pushes the service further past capacity, causing MORE requests
to fail/timeout, triggering MORE retries...
5. Positive feedback loop: the "fix" (retrying) has turned a partial degradation
into a complete outage, and the service may never recover on its own even after
the ORIGINAL cause of the initial slowdown is resolved, because the retry-driven
load has become self-sustaining.
Standard mitigations, each addressing a different part of the feedback loop:
- Exponential backoff — each successive retry waits progressively longer (1s, 2s, 4s, 8s...), reducing the rate at which retries pile onto a struggling service.
- Jitter — adding randomness to the backoff delay, so that many clients who failed at the same moment (a common pattern — they all failed on the same request wave) don't all retry at exactly the same moment again, which would just recreate a synchronized spike.
- Circuit breakers — after enough consecutive failures, a client stops sending requests to a struggling dependency entirely for a cooldown period, giving it room to recover instead of continuously adding failed-request load to it.
- Retry budgets — capping the proportion of traffic that's allowed to be retries (e.g., retries can add at most 10% extra load), preventing retries from ever dominating total traffic even under sustained partial failure.
Thundering herd: many workers waking for one event
[BEGINNER] A thundering herd occurs when a large number of processes/threads/clients are all waiting on the same event, and that event's occurrence wakes all of them simultaneously — even though typically only one (or a few) can actually usefully proceed, meaning most of the woken parties immediately go back to waiting after uselessly consuming resources (CPU, a lock attempt, a network request) checking whether they can proceed.
A classic example: a popular cache entry expires, and a thousand concurrent requests for that same resource simultaneously discover a cache miss and simultaneously stampede the origin/database to regenerate it — even though only one regeneration was actually necessary, all thousand requests independently trigger the expensive work at once.
Standard mitigation: a locking/single-flight pattern — the first request to discover the cache miss acquires a lock and does the expensive regeneration; every other concurrent request for the same key waits on that same in-progress regeneration instead of independently triggering it, and all of them receive the single, shared result once it completes.
import threading
_locks = {}
_lock_guard = threading.Lock()
def get_or_regenerate(key, expensive_fn):
with _lock_guard:
lock = _locks.setdefault(key, threading.Lock())
with lock: # only ONE caller per key actually runs expensive_fn; others wait here
cached = cache.get(key)
if cached is not None:
return cached
result = expensive_fn()
cache.set(key, result)
return result
Common mistakes
- Setting only one generic "timeout" value and assuming it covers connection, TLS, and request/response phases uniformly — each phase can fail independently and may need a different appropriate limit.
- Implementing retries with no backoff, no jitter, and no cap — this is precisely the configuration that turns a partial degradation into a full retry storm during real incidents.
- Not implementing a circuit breaker (or equivalent) for calls to a dependency that's known to sometimes struggle — without one, every single caller continues hammering a failing dependency indefinitely, with no mechanism to back off collectively.
- Not protecting against thundering herd on any shared, expensive-to-regenerate resource (a popular cache key, a rarely-changing but expensive computation) — assuming cache misses are independent, rare events rather than potentially simultaneous, correlated ones.
Checklist: building resilience against these failure modes
- Configure connection, TLS, and request/response timeouts explicitly and separately, rather than relying on a single generic value or framework defaults that may not fit your actual latency requirements.
- Tune application-level keep-alive traffic more aggressively than the most restrictive intermediary's idle timeout when experiencing mysterious idle-connection resets.
- Always implement retries with exponential backoff and jitter, and consider a retry budget that caps the proportion of total traffic retries can represent.
- Add circuit breakers for calls to dependencies that can degrade, so callers collectively back off during an incident instead of continuously adding load to an already-struggling service.
- Use a single-flight/locking pattern for any expensive, shared, cacheable computation to prevent a thundering herd on cache expiration or invalidation.
Discussion / interview questions
-
"Why might a service need more than one kind of timeout configured, rather than a single generic value?" Strong answer: Different phases of a request (TCP connection, TLS handshake, waiting for a response, idle keep-alive) can each fail or hang independently, and a single generic timeout either leaves some phases unprotected or applies an inappropriate limit to phases that need a different value.
-
"What is a retry storm, and how does exponential backoff with jitter help prevent one?" Strong answer: Naive immediate retries during a partial outage add load to an already-struggling service, pushing it further past capacity and triggering more failures/retries in a feedback loop; exponential backoff reduces the rate of retries over time, and jitter prevents synchronized retry waves from clients that all failed at the same moment.
-
"What is a thundering herd problem, and how would you prevent it for a popular cache key that just expired?" Strong answer: Many concurrent requests discovering the same cache miss simultaneously all trigger the same expensive regeneration independently; the standard fix is a single-flight/locking pattern where only the first request performs the regeneration while others wait for and share its result.
-
"What's the purpose of a circuit breaker, and how does it differ from a simple retry-with-backoff strategy?" Strong answer: A circuit breaker stops sending requests to a struggling dependency entirely after enough consecutive failures, giving it a cooldown period to recover; retry-with-backoff still eventually sends requests (just less frequently), while a circuit breaker temporarily stops entirely — a stronger intervention for a dependency that needs real recovery time rather than just reduced pressure.
Chapter summary / cheat sheet
- Timeouts should be set explicitly and separately for connection, TLS handshake, and request/response phases — a single generic value leaves gaps.
- Connection resets on idle connections are often caused by an intermediary's idle timeout, not either endpoint's own configuration — application-level keep-alive tuned more aggressively than the path's most restrictive timeout is the standard fix.
- Retry storms: naive retries during a partial outage create a self-sustaining feedback loop — exponential backoff, jitter, circuit breakers, and retry budgets are the standard, complementary mitigations.
- Thundering herd: many concurrent requests independently triggering the same expensive regeneration on a shared resource — a single-flight/locking pattern is the standard fix.
- All four failure modes share a theme: naive, locally-reasonable behavior (retry on failure, wait for an event, don't set arbitrary limits) can become actively harmful in aggregate, at scale, under real failure conditions — which is exactly why each has a specific, well-known, standard countermeasure.