Design safe retries: exponential backoff, jitter, budgets, idempotency, and when not to retry. Use when retry, backoff, 重试, transient failure, 429/503 handling, dead-letter, or retry storms. Complements async cancel and cache loaders; not a substitute for fixing non-idempotent APIs.
Design safe retries: exponential backoff, jitter, budgets, idempotency, and when not to retry. Use when retry, backoff, 重试, transient failure, 429/503 handling, dead-letter, or retry storms. Complements async cancel and cache loaders; not a substitute for fixing non-idempotent APIs.
Retry And Backoff Patterns
Engineering design for recovering from transient failures without creating
outages: what is safe to retry, how long to wait, how to bound total work, and
how to stay idempotent under duplicate execution. Prefer the repo’s existing
retry helpers, HTTP clients, and queue middleware over ad-hoc sleep loops.
Use When
Implementing or reviewing retries on HTTP/RPC, DB, message consumers, or SDKs
Choosing exponential backoff, caps, jitter, and attempt budgets
Handling 429 / 503, Retry-After, timeouts, and connection resets
Ensuring idempotency for retried side effects (payments, emails, writes)
Stopping retry storms that amplify origin or dependency outages
Neighboring code: copy 2–3 mature clients’ max attempts, backoff base/cap,
and non-retryable error lists before inventing new constants
Precedence: If repo rules conflict with defaults below, follow the repo.
Surface conflicts that retry non-idempotent writes, ignore Retry-After, or
stack retries across layers without a single budget.
Workflow
Classify the operation.
Read / pure: usually safe to retry if result is not “exactly once” sensitive
Idempotent write: safe with same key/body (PUT upsert, DELETE, GET-like)
Non-idempotent write: do not blind-retry without idempotency key or
dedupe store
Classify the error.
Transient: timeouts, connection reset, 408/425/429/500/502/503/504
(tune to API; not all 500s are safe)
Non-retryable: 400/401/403/404/422 validation, permanent business rejection
Ambiguous success: timeout after request may have committed — requires
idempotency or status probe, not naive re-POST
Define a budget (not infinite loops).
Max attempts or max wall-clock duration (prefer both)
Honor parent deadline/cancel; do not retry after abort
Choose delay policy (see Backoff And Jitter).
Honor server signals:Retry-After, rate-limit headers, gRPC retry info.
Make side effects safe: idempotency keys, dedupe tables, exactly-once
business outcome with at-least-once transport.
Observe: attempt count, final outcome, delay histogram; alert on retry
rate spikes (storm indicator).
Client sends unique key; server stores response for key+principal+route
Natural keys
Upsert on business id (PUT /resources/{id}) instead of blind insert
Dedupe store
Record processed messageId / eventId before side effects commit
Outbox
Durable intent then publisher retries without re-running business logic
State machine
Transitions accept only legal predecessors (ignore duplicate events)
Good: payment create with Idempotency-Key: uk_… and server returns same
result on retry. Bad: retry POST /charges with no key after a timeout (double charge risk).
Layering And Budgets
User request deadline ─────────────────────────────────────┐
Service A retry budget ──────────┐ │
Service B retry budget ────┐ │ │
Dependency timeout ──┐ │ │ │
Prefer one primary retry layer per hop; disable or sharply limit nested
SDK retries when the outer layer already retries.
Propagate deadlines so inner work does not outlive the user budget
(async-concurrency-patterns).
On fan-out, apply bulkheads and per-dependency concurrency limits so
retries cannot exhaust the whole process.
Messaging Consumers
Visibility timeout ≥ max processing time (including retries inside the handler).
Prefer broker redelivery or in-handler retry — not both unbounded.
After max attempts → DLQ with poison reason; alert; do not infinite loop.
Handlers must be idempotent under at-least-once delivery.