What it means
Retry-After is the server answering the only question a refused client actually has: when should I come back? It is one of the oldest response headers in HTTP, carried forward from RFC 2616 through RFC 7231 into RFC 9110, which is where it lives today, in section 10.2.3.
It is worth appreciating how much guesswork the header removes. A client without it has to invent a delay, which means either guessing short and getting refused again, or guessing long and being slower than necessary. A client with it knows. The whole value of the header is that the server has information the client cannot possibly have: how big the window is, how far into it you are, and when it rolls over.
The two formats, and the parser bug
The grammar allows exactly two forms.
- delay-seconds: a non-negative decimal integer.
Retry-After: 120means two minutes from now. - HTTP-date: an absolute moment in the preferred IMF-fixdate format.
Retry-After: Wed, 02 Sep 2026 12:00:00 GMT. The date is always in GMT.
Most clients only ever implement the first. The resulting bug is quiet and nasty: parseInt("Wed, 02 Sep 2026 12:00:00 GMT") returns NaN, and code that does const wait = parseInt(header) || 0 retries immediately, in a loop, against a server that just asked for a pause. Parse both forms, and treat a date in the past as zero rather than as a negative sleep.
Servers should prefer the seconds form, for a reason that is purely operational. The date form is only as good as the client's clock. Client clocks drift, virtual machines resume with stale time, and embedded devices boot at the epoch. A duration cannot be mis-parsed by a wrong clock; an absolute timestamp can.
Where it legitimately appears
RFC 9110 describes Retry-After on a 503 to say how long the service expects to be unavailable, and on any 3xx response to give the minimum time before issuing the redirected request. The most common use in practice is on 429 Too Many Requests, which is defined separately in RFC 6585 section 4 and explicitly invites the header. You will also see it on 202 Accepted as a polling hint, which is a convention rather than a specification.
It is a floor, not a target
This is the sentence that separates a client that behaves well from one that causes a second incident. If ten thousand clients are refused at time T with Retry-After: 12, and every one of them sleeps exactly twelve seconds, then at T plus 12 the server receives ten thousand simultaneous requests. You have not spread the load, you have scheduled a stampede with the server's own cooperation.
The correct handling is to treat the value as a minimum and add randomness on top:
wait = retry_after + random(0, min(retry_after, 5 seconds))
Never subtract. Retrying before the stated time is a request the server has already told you it will refuse, and on a metered API that refusal still costs you a unit of quota. See jitter for the strategies and the arithmetic behind them.
Retry-After also outranks whatever your exponential backoff curve computed. The rule is delay = max(computed_backoff, retry_after), then jitter. Your curve is the fallback for responses that carry no header at all, which is most 500s and 502s.
Read the error code before you read the header
A 429 in the Pinlyx Data API comes in two flavours and they need opposite reactions. rate_limited means the burst ceiling for the current minute is spent, and it clears within that minute, so obeying Retry-After is exactly right. quota_exceeded means the daily quota is gone and it does not clear until 00:00 UTC.
A client that reads only the header and only the status will keep waking up, retrying and being refused for the rest of the day. A client that reads error.code first stops, logs, and reschedules for tomorrow. The status code is the category; the error code is the diagnosis. The same applies to 503, where upstream_timeout is worth retrying, upstream_error wants a longer wait, and not_configured will never succeed no matter how long you sleep.
One more warning: the human-readable message often repeats the number, as in "Rate limit exceeded. Retry in 12 seconds." That text is prose and may change. Do not parse it. The header is the machine-readable channel and the code is the stable branch point.
Emitting it on your own API
If you are the server, three rules cover almost everything.
- Send it on every 429 and every planned 503. A 429 without a Retry-After forces every client to guess, and their guesses will be worse than your answer.
- Make it truthful. A constant
Retry-After: 60stamped on every refusal is worse than nothing: it is wrong for most callers, and it synchronises all of them onto the same one-minute grid. - Keep it consistent with your other headers. If you also publish
X-RateLimit-Resetas an absolute unix second, the two must agree. A client that sees a 12-second Retry-After and a reset timestamp 90 seconds out has no idea which to believe.
Related concepts
- Rate limit: the ceiling whose refusal carries this header.
- Exponential backoff: what to do when there is no header to read.
- Jitter: why obeying the header exactly is not enough.
- Flood wait: the same idea outside HTTP, delivered as a protocol error with a seconds value.
- Idempotency key: what makes the eventual retry safe to send.
How Pinlyx handles it
Every 429 from the Data API carries Retry-After in seconds, alongside X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and the daily quota headers, so a client can tell not only how long to wait but which of the four stacked ceilings it hit. The two 429 error codes are kept distinct precisely so that a one-minute pause is never confused with a wait until midnight.