Reliability guide
How do you review retry logic safely?
Published
Quick answer
Review retry logic by tracing one attempt from the request to every observable side effect, then ask five questions: can another attempt succeed, can the operation repeat without duplicating work, which layer owns the retry budget, how are attempts spaced, and what happens when that budget ends? A retry is not safe merely because it catches an error. When an operation is non-idempotent or its outcome is unknown, verify the operation's contract or reconcile the resulting state before trying again.
Classify the failure before adding another attempt
A retry can help when a failure is temporary and a later attempt has a reasonable chance of succeeding. Validation failures, permission failures, and malformed requests usually need a changed request or changed authority instead. Retrying them automatically spends time and capacity without changing the condition that failed.
A timeout deserves special care because it describes what the caller observed, not necessarily what the server did. The server might have rejected the operation, still be processing it, or have completed the side effect before the response was lost. Mark that outcome as unknown until the system's contract gives you evidence otherwise.
Trace effects, not only return values
An operation is idempotent when repeating the same request is intended to have the same effect as making it once. That does not require every response to be byte-for-byte identical. Setting a record to a known state can be repeatable, while incrementing a counter, sending a message, or creating a new charge can produce another effect on every attempt.
For each retryable call, list the durable writes, messages, external requests, and user-visible actions it can trigger. Then find the mechanism that makes a repeated attempt safe: a stable operation key, a uniqueness constraint, a compare-and-set condition, or a reconciliation step that checks whether the first attempt already took effect.
Give one layer a bounded retry budget
Retries at several layers can multiply calls when a dependency is already struggling. Pick the layer that has enough context to classify the failure and own the policy. Record the maximum attempts or deadline there so callers and inner clients do not each start an independent retry loop.
Use delay and jitter to avoid sending every retry back at the same instant, but do not confuse pacing with safety. Backoff cannot make a permanent error temporary, prevent a duplicate side effect, or decide how long the user can wait. The total deadline still has to fit the caller's latency and cancellation contract.
Review the stopping path as carefully as success
When the budget ends, the code should preserve enough evidence for the caller and operator to act: the final error, the number of attempts, relevant timing, and whether the outcome is known or uncertain. A generic fallback can hide a partial success and make later reconciliation harder.
Test at least four paths: immediate success, a temporary failure followed by success, a permanent failure with no retry, and an exhausted or ambiguous attempt. Use a controllable clock or scheduler so the test verifies the policy without sleeping in real time.
A retry review decision table
Use this original decision aid to separate what the caller observed from what the operation might already have changed. The table does not choose a library or status-code list for you; it identifies the evidence the policy still needs.
| Observed condition | Review decision | What to verify |
|---|---|---|
| A validation or authorization rejection | Do not retry the same request automatically. | Identify which input, permission, or policy would have to change before another attempt could produce a different result. |
| A timeout after the request was sent | Treat the operation's outcome as unknown. | Find a stable idempotency mechanism or a read/reconciliation path that can establish whether the first attempt took effect. |
| A documented temporary capacity or availability failure | Retry only inside the owning layer's budget. | Verify the dependency contract, any server-provided delay, the total deadline, and a bounded backoff policy with jitter. |
| An operation whose intended effects can safely repeat | A retry may be appropriate, but it still needs limits. | Show why effects are idempotent, which layer owns the attempts, how cancellation propagates, and what the caller receives at exhaustion. |
A small practice loop
- 01Choose one retry wrapper and draw the boundary of a single attempt, including every durable write and external side effect.
- 02Make a table for each failure with its retry classification, outcome certainty, safety mechanism, policy owner, and stopping condition.
- 03Add focused tests for temporary success, permanent failure, an ambiguous outcome, and an exhausted retry budget.
- 04Check that logs and returned errors distinguish a failed operation from an operation whose final outcome still needs reconciliation.
Common questions
Is a POST request never safe to retry?
No. The method alone does not settle the application's semantics. A client needs evidence that repeating the operation is safe, such as a documented idempotency mechanism, or evidence that the original request was never applied. Without that evidence, an automatic retry can repeat a side effect.
Does exponential backoff make retry logic safe?
No. Backoff changes when attempts happen. It does not make a permanent failure retryable, stop nested policies from multiplying work, or prevent duplicate side effects. Review idempotency, ownership, limits, jitter, deadlines, and the final failure path separately.
Limits of this guide
This guide covers application-level retry review, not a universal list of retryable exceptions or status codes. Broker delivery guarantees, database transactions, provider-specific idempotency windows, cancellation behavior, and business reconciliation rules can change the safe decision. Verify those contracts in the system you are reviewing, and test the real integration before relying on a small example or checklist.
Sources and scope
This page is an original editorial synthesis by Unrust. Its protocol claims were checked against RFC 9110, its load and pacing guidance against the AWS Builders Library, and its idempotency-key example against Stripe's public API documentation. The decision table and wording were created for this guide rather than copied from those sources.
- RFC 9110, Section 9.2.2: Idempotent Methods
RFC Editor
Defines idempotent request semantics and cautions against automatically retrying a non-idempotent method without evidence that repetition is safe or the original request was not applied.
- Timeouts, retries, and backoff with jitter
AWS Builders Library
Describes Amazon's operational experience with retry amplification, bounded attempts, backoff, and jitter. It is vendor guidance and experience, not a universal protocol standard.
- Idempotent requests
Stripe API Reference
Documents one concrete API design in which the first result is associated with an idempotency key and later requests using that key return the saved result; the behavior is specific to Stripe's contract.
Check your own starting point
Unrust uses short, timed, unassisted drills to give developers a per-skill snapshot. One session cannot explain the cause of a result, but it can show you what is worth practicing next.
Take the free diagnostic