Timeouts: three layers, not one
A single "total timeout" is usually not enough. Split it into three: a connect timeout (1–3s for TCP/TLS), a read timeout (based on the endpoint's P99), and an overall retry budget (a cap across all attempts). Without a budget, one blip can generate minutes of repeated requests and take the downstream down with it.
Retry only failures that can succeed
| Failure | Retry? | Guidance |
|---|---|---|
| Connect timeout, 502 / 503 / 504 | Yes | Exponential backoff with jitter |
| 429 (rate limited) | Carefully | Respect Retry-After and cap attempts |
| 408 request timeout | Context-dependent | Requires an idempotency key |
| 400 / 401 / 403 / 404 / 422 | No | The request itself is wrong |
| 500 | Carefully | Only when known to be transient |
Exponential backoff with jitter
// Wait before attempt n (ms), with jitter to avoid a retry storm
const base = 200, cap = 10000;
const wait = Math.min(cap, base * 2 ** attempt);
const jitter = Math.random() * wait * 0.3;
await sleep(wait + jitter);
Backoff without jitter makes every client retry at the same instant, producing a retry storm. Jitter spreads attempts across a window and flattens the peak.
Idempotency: what makes retries safe
Idempotent means executing the same request many times has the same effect as once. Without that guarantee, retries cause duplicate charges, duplicate orders or duplicate messages. Common practice:
- The client generates an idempotency key (e.g. a UUID) per business operation and sends it with the request;
- The server records "key → first result" and replays that result instead of executing again;
- Keys expire (e.g. after 24 hours) so storage does not grow forever;
- Prefer writes that set a value over ones that increment (setBalance beats addBalance).
Four common mistakes
- Retries amplifying traffic: without caps and a budget, a blip becomes an avalanche;
- Retrying at several layers: gateway retries 3 and the app retries 3 — that is 9 requests;
- Retrying non-idempotent calls: duplicate money movement or messages;
- Over-long timeouts: connections and threads exhaust, and the failure spreads faster.
- Compute timeouts bottom-up: fix the outermost acceptable latency, then allocate inward, keeping each upstream timeout larger than the sum below it — or upstreams abandon and retry while downstreams are still working.
- Bound and taper retries: attempts and backoff should decrease as you approach the edge, so one blip is not multiplied into historic traffic by many layers.
- Retry only retryable errors: retrying a bad parameter or failed authentication is pointless and adds load; only connection failures, timeouts and some server errors deserve it.
- Reads and writes differ: reads retry automatically; writes need an idempotency key or a retry duplicates the effect.
- Measure retries: report retry rate, retry success rate and the extra latency they add. A persistently high retry rate means the dependency is unstable and needs fixing.
Observability and debugging
Attach a request ID (generate one with a UUID tool) to every call and log attempt count, duration and final status. When something breaks, look at the distribution of retry counts and latencies first — that usually separates a network blip, a slow downstream and a bad retry policy.
Try it: UUID generator — create request IDs and idempotency keys
Timeout and retry budgets
In distributed calls, timeouts and retries must be allocated as one budget rather than guessed per service.
Idempotency key details
The client should generate the key with business meaning; the server stores the result for a retention window and returns the first outcome for duplicates. Mind storage cost and expiry, and serialise concurrent requests carrying the same key so both do not execute halfway.