skip to content

An HTTP client sends a request, the TCP connection drops, and no response ever arrives. Under HTTP's own rules, when may the client automatically retry that request, and what must it never retry automatically?

level: seniorimportance: must knowfreq 55%

answer

  1. No response + idempotent method → auto-retry OK
  2. POST/PATCH: lost response ≠ lost request → never auto-retry
  3. Not-processed signals: REFUSED_STREAM, GOAWAY id, 421, 425, 408
  4. 503/429 + Retry-After = declined, not processed
  5. Timeout is more dangerous than reset; backoff + jitter always

basics

~20 s

A client may automatically retry only if the method is idempotent (GET, HEAD, OPTIONS, TRACE, PUT, DELETE) and it received no response — or if the server explicitly signalled the request was not processed. Never auto-retry POST or PATCH: a lost response is indistinguishable from a lost request, so the retry may duplicate the effect.

solid answer

~50 s

RFC 9110 §9.2.2 permits a client to **automatically** retry a request when the method is idempotent and the response was not received — a connection reset, timeout, or close before any status line. That is the whole license, and it exists because with an idempotent method it does not matter whether the original arrived. A client must **not** automatically retry a non-idempotent request (POST, PATCH). The client cannot distinguish "the request never reached the server" from "it was processed and the response was lost", so retrying risks a duplicate charge or order. A human or application-level policy may retry; the transport must not. Exceptions where even POST is safely retryable, because the server has told you it did nothing: HTTP/2 `REFUSED_STREAM`, streams above a GOAWAY's last-stream-id, HTTP 421 Misdirected Request, and 425 Too Early for TLS early data. 503 and 429 with `Retry-After` also indicate the request was rejected rather than processed. Everything else — deduplication for POST — is an application contract, not an HTTP rule.

code

http · 9 lines
http
HTTP/1.1 421 Misdirected Request

HTTP/1.1 425 Too Early

HTTP/1.1 503 Service Unavailable
Retry-After: 30

HTTP/2 RST_STREAM (error code REFUSED_STREAM)
HTTP/2 GOAWAY (last-stream-id < this request's stream id)

go deeper

for a junior

Say retries are safe for idempotent methods and unsafe for POST, and explain the core reason: you cannot tell a lost response from a lost request.

for a middle

Add the specific signals — 408, 503 with Retry-After, 421 — and note that backoff with jitter is required even for idempotent retries.

for a senior

Own the operational picture: which layer is allowed to retry, auditing client/proxy/mesh defaults, HTTP/2 REFUSED_STREAM and GOAWAY as explicit not-processed signals, and why timeouts are the worst case.

for a principal

Set org-wide policy — retry budgets, circuit breakers, which tiers may retry so retries don't multiply across hops, and where non-idempotent operations must carry an explicit deduplication contract instead of being retried.

## The rule RFC 9110 §9.2.2 states that a client **SHOULD NOT** automatically retry a request that is not idempotent, and that automatic retry is appropriate when the method is idempotent and the client received no response. Everything else in this area follows from that sentence plus one hard fact about networks. ## The fact that makes it hard: the ambiguity of silence When a connection dies with no response, exactly one of these happened, and the client cannot tell which: 1. The request never reached the server. 2. The request reached the server, was fully processed, and the **response** was lost. 3. The request was partially processed and the server crashed midway. Case 2 is the killer. If the method is POST `/payments`, a retry in case 2 charges the card twice. If the method is `PUT /users/42` with a full representation, a retry in case 2 writes the same bytes again and nothing is harmed. That asymmetry *is* the retry rule — idempotency is precisely the property that makes the ambiguity irrelevant. So the guarantee HTTP gives an automatic retrier is **at-least-once with convergent effect** for idempotent methods, and nothing at all for non-idempotent ones. ## What may be retried automatically - **Idempotent methods with no response received.** GET, HEAD, OPTIONS, TRACE, PUT, DELETE. This is what connection pools do when they hand out a stale keep-alive connection that the server has already closed: the request goes out, the socket resets, and the client silently re-dispatches on a fresh connection. Almost every HTTP client library implements exactly this and nothing more. - **Any method, when the server has explicitly signalled it did not process the request.** These signals are what let you retry a POST safely: - **HTTP/2 `REFUSED_STREAM`** — the RST_STREAM error code defined to mean "this stream was not processed"; the request may be retried on another connection. - **GOAWAY with a last-stream-id below your stream id** — the server is telling you precisely which streams it did not handle. - **421 Misdirected Request** — this connection cannot serve the target authority; the request wasn't processed here, retry elsewhere. - **425 Too Early** — the server refuses to process TLS 1.3 early data (0-RTT), which could be replayed; retry after the handshake completes. - **503 Service Unavailable / 429 Too Many Requests**, typically with `Retry-After` — the server is declining, not processing. Retry after the indicated delay, with backoff and jitter. - **408 Request Timeout** — the server gave up waiting for the request; nothing was processed, so the request may be repeated. ## What must not be retried automatically **POST and PATCH after an ambiguous failure.** No transport-level component may do this: not the client library, not the connection pool, not the reverse proxy, not the service mesh sidecar. If middleware in your stack retries POSTs on timeout by default, that is a production incident waiting for its trigger, and it is one of the first things worth auditing in a payments path. Also worth stating: a **timeout is not a failure signal**. When a client times out, the server may still be working and may complete successfully seconds later. Retrying on client-side timeout is strictly more dangerous than retrying on a connection reset, because the request definitely reached the server. ## The nuance interviewers push on **"But we retry POSTs all the time."** Yes — with an application-level contract that makes the duplicate detectable, so the retry is a deliberate design decision rather than a transport default. HTTP itself gives you no such mechanism; you are layering one on top, and both sides must agree on it. **Idempotent does not mean harmless to retry infinitely.** A retry storm from a fleet of clients hammering an already-degraded server is a self-inflicted outage, whatever the method. Exponential backoff with jitter, a bounded retry count, honouring `Retry-After`, and a circuit breaker are all still required. Idempotency licenses the retry; it does not size it. **Non-idempotent requests can be made retry-safe by narrowing the ambiguity.** If the client can cheaply query whether the effect happened — "does an order with my client reference exist?" — it can resolve case 2 by reading rather than guessing. That read-then-decide pattern is often simpler than it looks and needs no new headers. **Where the bytes stopped matters less than people think.** Some libraries retry only if no request bytes were written. That is a sound heuristic but not a guarantee: the server may have received and processed everything and died before responding. Only idempotency, or an explicit not-processed signal, makes retry genuinely safe. ## Summary Automatic retry is licensed by idempotency or by an explicit "I did not process this" signal from the server. Absent both, a retry is a guess about what happened on the other side, and for POST that guess costs money.

  • Why is retrying after a client-side timeout more dangerous than retrying after a connection reset?
    A timeout means the request definitely reached the server and may still be executing or may have completed with the response lost in transit. A reset before any bytes were written makes it much likelier the request never landed. Neither is a guarantee, which is why only idempotency or an explicit not-processed signal makes a retry genuinely safe.
  • Your service mesh retries all failed requests, including POSTs, by default. What do you do?
    Turn method-blind retry off and restrict automatic retries to idempotent methods plus explicit not-processed signals such as HTTP/2 REFUSED_STREAM, 421 and 503 with Retry-After. Non-idempotent endpoints that need retry get an application-level deduplication contract instead, so the duplicate is detectable at the server rather than assumed away by the proxy.
  • HTTP/2 gives you REFUSED_STREAM and GOAWAY; what is the HTTP/1.1 equivalent?
    There isn't one. HTTP/1.1 has no frame-level way for a server to say 'I did not process this request', so a client only has connection-level heuristics — whether the socket reset before or after the request was written — plus status codes like 408 and 503. That is precisely why HTTP/2 clients can retry more aggressively and more safely than HTTP/1.1 clients.

Posting a letter and hearing nothing: if the letter says 'set the thermostat to 20', sending another copy is free. If it says 'add 20 to the thermostat', you have no idea whether the first one arrived.

saying these in an interview costs you the question

  • Retrying POST automatically on timeout because 'it probably failed'.
  • Believing a lost response means the request was not processed.
  • Thinking idempotency alone makes unlimited retries safe, with no backoff, jitter or cap.
  • Not knowing any explicit not-processed signal (REFUSED_STREAM, GOAWAY last-stream-id, 421, 425).
  • Treating a 500 response as retry-safe — the server responded, and it may have committed a partial effect.

context