A timeout does not prove the server did nothing. It may have completed a POST before the response disappeared. Retrying blindly can create two purchases, messages, or workout sessions.
I make retry behavior opt in:
enum RetryPolicy {
case none
case idempotent
}
The client applies the idempotent policy to safe reads by default and rejects accidental use on POST, PATCH, or DELETE unless the caller explicitly supplies a server-supported idempotency design.
Retry connection failures and selected temporary gateway statuses. Do not retry validation, authorization, decoding, or ordinary conflicts.
Use a small attempt limit with exponential backoff and jitter:
500 ms ± jitter
1 s ± jitter
stop
Respect Retry-After for rate limits when it is reasonable. Cancellation should stop sleeping and end the operation immediately.
For writes that are safe to replay, send a stable idempotency key and reuse it across attempts. A newly generated key on every retry defeats the guarantee.
Test the policy with a controlled URL protocol: temporary failure followed by success, permanent failure, cancellation during backoff, and a write that must remain single-attempt. Reliability comes from knowing which operations may repeat, not from retrying everything that failed.