GLOSSARY

What is the Retry-After header?

Retry-After is a standard HTTP response header, defined in RFC 9110, that tells the client how long to wait before repeating the request. Its value is either a number of seconds or an HTTP-date.

Free forever plan · No credit card required · Cancel anytime

Quick definition

Retry-After is a standard HTTP response header, defined in RFC 9110, that tells the client how long to wait before repeating the request. Its value is either a number of seconds or an HTTP-date.

The grammar, from RFC 9110: Retry-After = HTTP-date / delay-seconds

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: 120 means 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: 60 stamped 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-Reset as 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.

Cheat sheet · both formats, on the wire

The same instruction, two encodings.

delay-seconds, on a real 429
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1756547412
X-Quota-Limit: 1000
X-Quota-Remaining: 814
X-Request-Id: req_9f2c41a8b3d5

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry in 12 seconds.",
    "request_id": "req_9f2c41a8b3d5"
  }
}
HTTP-date, on a maintenance 503
HTTP/1.1 503 Service Unavailable
Retry-After: Wed, 02 Sep 2026 12:00:00 GMT
Content-Type: application/json

{
  "error": {
    "code": "upstream_error",
    "message": "A data source did not answer in time. Try again.",
    "request_id": "req_9f2c41a8b3d5"
  }
}

# Both are legal per RFC 9110 section 10.2.3.
# The date form is only as accurate as the client's clock.

On the left, note that X-Quota-Remaining is still 814. The day is fine; only this minute is spent. Had the code been quota_exceeded, obeying a 12-second Retry-After would have produced a refused request every twelve seconds until midnight UTC.

Where you will meet it
StatusWhat it means thereWhat the client should do
429 Too Many RequestsYou are going faster than the limit allows.Sleep for the stated seconds plus jitter, then continue. Check the error code first: a daily quota may not clear for hours.
503 Service UnavailableThe service is temporarily unable to handle the request.Sleep, but branch on the error code. Some 503 conditions never resolve by waiting.
301 / 302 / 307 / 308A redirect that asks the client to pause before following it.Rare in practice, but RFC 9110 defines it: the minimum time to wait before issuing the redirected request.
202 AcceptedNon-standard but common: a hint at when a long-running job might be ready.Treat it as advisory polling guidance, not a guarantee. RFC 9110 does not define this use.
Cheat sheet · a parser that handles both forms

Twelve lines that remove a whole class of retry bug.

/** Returns milliseconds to wait, or 0 when there is nothing useful to read. */
export function parseRetryAfter(header: string | null, now = Date.now()): number {
  if (!header) return 0;
  const value = header.trim();

  // Form 1: delay-seconds. RFC 9110 says non-negative decimal integer.
  if (/^\d+$/.test(value)) return Number(value) * 1000;

  // Form 2: HTTP-date. A date in the past means "now", never a negative sleep.
  const at = Date.parse(value);
  if (Number.isNaN(at)) return 0;
  return Math.max(0, at - now);
}

/** Retry-After is a FLOOR. Add jitter, and cap against your own deadline. */
export function waitFor(header: string | null, budgetMs: number): number | null {
  const floor = parseRetryAfter(header);
  if (floor > budgetMs) return null;              // give up instead of parking
  const spread = Math.min(floor, 5_000);
  return floor + Math.random() * spread;
}

Returning null rather than sleeping is the part teams skip. A client that obeys an 86,400-second Retry-After inside a request handler has turned somebody else's rate limit into its own outage.

Watch out for

Six ways Retry-After gets mishandled.

  • parseInt on the header, which turns the date form into NaN and then into an immediate retry.
  • Sleeping exactly the stated time, so every refused client wakes in the same second.
  • Obeying it without a cap, and parking a worker for hours on a single header value.
  • Ignoring it and using a hard-coded delay, which is either refused again or needlessly slow.
  • Reading the header but not the error code, so a daily quota is retried every twelve seconds until midnight.
  • On the server side, stamping a constant value on every refusal, which is both untrue and perfectly synchronising.

Retry-After: FAQ

The formats, the precedence rules, and the parser bug almost everyone ships once.

RFC 9110 section 10.2.3 defines Retry-After as either delay-seconds, a non-negative decimal integer, or an HTTP-date. So "Retry-After: 120" and "Retry-After: Wed, 02 Sep 2026 12:00:00 GMT" are both valid and mean roughly the same thing. A parser that assumes one form breaks on the other, and the failure is silent: parseInt on a date string yields NaN, which many clients quietly treat as zero and retry immediately.
Seconds, almost always. The date form depends on the client having a correct clock, and a meaningful share of clients do not. A client whose clock is twenty minutes slow will treat a date-form Retry-After as already past and hammer the server it was asked to leave alone. The seconds form is a duration and is immune to clock skew. Send a date only when the resume time is genuinely a wall-clock moment, such as a scheduled maintenance window.
No. It is a floor, not a guarantee. The server is saying "not before this", not "definitely available at this". Your client still needs a retry policy, a cap on total attempts and a deadline. Treat Retry-After as the minimum wait and keep your own backoff curve as the fallback when the header is absent.
Cap it against your own deadline. A misconfigured or hostile server can answer Retry-After: 86400, and a client that sleeps 24 hours inside a request handler has become the outage. The rule is wait = min(retry_after, your_remaining_budget), and if the header exceeds the budget, fail the operation now and let the caller decide. That is a much better outcome than a thread parked for a day.
Almost nothing. Browsers do not sleep and re-issue a fetch for you, and neither do fetch, axios, requests, HttpClient or curl by default. A handful of HTTP client libraries and service meshes implement it in their retry middleware, and the AWS and Google Cloud SDKs handle their own variants. Assume it is your code that has to read the header, or it will be ignored.
They should agree, and they encode the same fact differently. In the Pinlyx Data API a 429 carries both: Retry-After is a duration in seconds, X-RateLimit-Reset is the absolute unix second at which the window resets. Retry-After should equal X-RateLimit-Reset minus now. Prefer Retry-After, because the duration form cannot be broken by a wrong local clock; use the reset timestamp when you want to display "limit resets at 14:32" in a dashboard.
Ready to ship

Stop guessing. The server already knows.

Retry-After on every 429, alongside the burst, quota and plan headers, so a client can tell a one-minute pause from a wait until midnight.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.