Idempotency & Retry-Safe Systems · Why Retries Happen and What Idempotency Means
HTTP Semantics and Retry Policies
Which requests clients may retry automatically, and how servers signal retry behavior.
Generic HTTP clients may retry idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS) after connection failures; they must not retry POST/PATCH unless the API guarantees retry-safety (idempotency keys). Servers guide retries with status codes: 429 and 503 with Retry-After mean "retry later"; most other 4xx mean "don't retry without changing the request"; 500/502/504 are retryable only for idempotent or keyed requests.
Retry decision table
| Response / error | Retry? | How |
|---|---|---|
| Connection error / timeout | If idempotent or keyed | Backoff + jitter |
| 429 Too Many Requests | Yes | After Retry-After |
| 503 Service Unavailable | Yes | After Retry-After / backoff |
| 500 / 502 / 504 | If idempotent or keyed | Backoff, limited attempts |
| 409 Conflict (key in use) | Yes, later | Same idempotency key |
| 400 / 401 / 403 / 404 / 422 | No | Fix request or credentials |
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
export function shouldRetry(method: string, status: number | null, keyed: boolean): boolean {
const idempotent = ["GET", "HEAD", "PUT", "DELETE", "OPTIONS"].includes(method) || keyed;
if (status === null) return idempotent; // network error / timeout
if (status === 429 || status === 503) return true;
return idempotent && RETRYABLE.has(status);
}
RETRYABLE = {429, 500, 502, 503, 504}
def should_retry(method: str, status: int | None, keyed: bool) -> bool:
idempotent = method in {"GET", "HEAD", "PUT", "DELETE", "OPTIONS"} or keyed
if status is None: # network error / timeout
return idempotent
if status in (429, 503):
return True
return idempotent and status in RETRYABLE
func shouldRetry(method string, status int, keyed bool) bool {
idempotent := keyed || method == "GET" || method == "HEAD" || method == "PUT" || method == "DELETE"
switch {
case status == 0: // network error / timeout
return idempotent
case status == 429 || status == 503:
return true
case status >= 500:
return idempotent
}
return false
}
Common pitfalls
- Retrying 4xx validation errors.
- Ignoring Retry-After.
- Proxies configured to retry POSTs.
Key points
- Retry idempotent or keyed requests only
- Honor Retry-After on 429/503
- Don't retry most 4xx
Further reading: RFC 9110: Retry-After
Practice questions
Why might a load balancer retrying POST requests cause duplicate orders?
Answer
If the backend processed the POST but the response was slow or lost, the LB's retry sends it again to another instance, creating a second order unless the API deduplicates.
What should a client do with a 409 meaning 'request with this idempotency key is still in progress'?
Answer
Wait (backoff) and retry with the same key until the original completes, then receive the stored result.
Open in the app to mark lessons complete, take notes and track your progress.