How to test retry-after header

Get yourself refused on a target you control, then read the field off the 429 or the 503: curl -s -i http://127.0.0.1:8788/search. It carries either a count of seconds or an HTTP date, and a client has to handle both. Waiting exactly that long and getting 200 is the test.

Why check this

Retry-After is the one piece of a failure response that a client is expected to act on rather than log. Test it whenever an API starts returning 429 or 503 on purpose, and again after a maintenance window is added, because the two situations usually produce the two different formats and most clients parse only one. A client that misreads the date form sleeps for zero seconds, hammers the service that is already down, and turns a two minute outage into a retry storm.

Prerequisites

// retry-after.js  /search answers 429 with delay-seconds, /maintenance answers 503 with an HTTP-date
const http = require('node:http');
const LIMIT = 2;
const WINDOW = 20;
let win = null;

http.createServer((req, res) => {
  if (req.url === '/maintenance') {
    res.setHeader('retry-after', new Date(Date.now() + 120_000).toUTCString());
    res.writeHead(503, { 'content-type': 'application/json' });
    res.end('{"error":"service unavailable"}');
    return;
  }
  const now = Math.floor(Date.now() / 1000);
  if (!win || now >= win.reset) win = { count: 0, reset: now + WINDOW };
  win.count += 1;
  if (win.count > LIMIT) {
    res.setHeader('retry-after', String(win.reset - now));
    res.writeHead(429, { 'content-type': 'application/json' });
    res.end('{"error":"too many requests"}');
    return;
  }
  res.writeHead(200, { 'content-type': 'application/json' });
  res.end('{"ok":true}');
}).listen(8788, '127.0.0.1');
# parse-retry-after.sh  prints the wait in seconds for either form of the header
for path in /search /maintenance; do
  ra=$(curl -s -o /dev/null -D - "http://127.0.0.1:8788$path" | tr -d '\r' | sed -n 's/^[Rr]etry-[Aa]fter: //p')
  case "$ra" in
    '')       printf '%-13s no retry-after\n' "$path" ;;
    *[!0-9]*) printf '%-13s date  %s -> %s s\n' "$path" "$ra" "$(( $(date -d "$ra" +%s) - $(date +%s) ))" ;;
    *)        printf '%-13s delay %s -> %s s\n' "$path" "$ra" "$ra" ;;
  esac
done

Steps

  1. Step 1.

    Send three calls to the limited route so the third one is refused.

    for i in $(seq 1 3); do curl -s -o /dev/null -w "$i %{http_code} retry-after=%header{retry-after}\n" http://127.0.0.1:8788/search; done
    
    1 200 retry-after=
    2 200 retry-after=
    3 429 retry-after=19

    The field appears only on the refusal, and it says 19 rather than 20 because the third call landed one second into the window. A value that counts down between calls is the sign of a live calculation rather than a constant.

  2. Step 2.

    Read the whole refusal so you can see what a client receives.

    curl -s -i http://127.0.0.1:8788/search
    
    HTTP/1.1 429 Too Many Requests
    retry-after: 19
    content-type: application/json
    Date: Fri, 11 Sep 2026 19:13:53 GMT
    
    {"error":"too many requests"}

    This is the delay-seconds form: a plain non-negative integer, counted from the moment the response was produced. Compare it with the Date header to see where the countdown starts.

  3. Step 3.

    Read the other format on a route that answers 503.

    curl -s -i http://127.0.0.1:8788/maintenance
    
    HTTP/1.1 503 Service Unavailable
    retry-after: Fri, 11 Sep 2026 19:15:53 GMT
    content-type: application/json
    Date: Fri, 11 Sep 2026 19:13:53 GMT
    
    {"error":"service unavailable"}

    Same field, an absolute instant instead of a duration. The wait is that time minus the Date header, which is two minutes here. Using the local clock instead of Date imports any skew between the two machines.

  4. Step 4.

    Turn both formats into seconds with the one piece of logic a client needs.

    bash parse-retry-after.sh
    
    /search       delay 19 -> 19 s
    /maintenance  date  Fri, 11 Sep 2026 19:15:53 GMT -> 120 s
    

    One branch on whether the value is all digits covers every legal case. If your client has no such branch, it handles one format and silently mishandles the other.

  5. Step 5.

    Hand the header to a client that obeys it and measure the wall clock.

    time curl -s --retry 1 --retry-max-time 60 -o /dev/null -w 'final status %{http_code}\n' http://127.0.0.1:8788/search
    
    final status 200
    
    real	0m19.043s

    curl waited 19.043 s, the exact number the server sent, then repeated the request and got 200. That agreement between the advertised wait and the measured wait is the result this procedure is after.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | retry-after: 19 | Delay-seconds form, counted from the response | Sleep that many seconds, then retry once. | | retry-after: Fri, 11 Sep 2026 19:15:53 GMT | HTTP-date form, an absolute instant | Subtract the Date header, not the local clock, then sleep the difference. | | retry-after: 0 | Retry immediately, which is legal | Keep a floor in the client, or one refusal becomes a tight loop. | | A date already in the past | The server clock or the value is wrong | Clamp to a minimum wait and report it. A negative difference must never become a zero delay by accident. | | No retry-after on a 429 or 503 | The client is refused with no guidance | Fall back to exponential backoff and file the missing field as a defect. | | retry-after: 86400 on a 429 | The quota is gone for a day | Check this is intended. A client that obeys it stops for 24 hours. |

Common mistakes

Sign: The client retries instantly against a 503 that carries a date.Cause: Passing an HTTP-date to a function that expects an integer yields zero or NaN, and most retry helpers treat that as no delay. The seconds form works in test, the date form appears only during a real maintenance window, so the defect ships.
Sign: The measured wait is a few seconds longer or shorter than the header said.Cause: The date form is resolved against the local clock instead of the Date header of the same response. Any skew between the two machines lands directly in the delay, and a client ahead of the server retries before the window has closed.
Sign: A test asserts retry-after equals 20 and fails intermittently.Cause: A live server computes the remaining window at the moment of the response, so the value falls as the window runs out. Step 1 returned 19 for that reason. Assert a range bound by the window, or assert that the value decreases.
Sign: The response carries both ratelimit and retry-after with different numbers.Cause: They answer different questions and the draft settles the conflict: Retry-After takes precedence and the effective window may be ignored. A client that prefers the quota field can retry too early and burn the next window.

Thresholds

Retry-After: 120

is two minutes, the literal example the specification gives. The delay form is a non-negative decimal integer of seconds, so a client that treats it as milliseconds retries 1000 times too soon.

Source: RFC 9110, section 10.2.3

What to check next

FAQ

What does retry-after mean on a 429 response?

It is how long the server wants you to wait before sending the request again. RFC 6585 defines 429 and says the response may carry the field. The value is advice a client should obey, not a promise that the next call succeeds.

Which format should an API send, seconds or a date?

Seconds for a wait the server computes per request, such as the tail of a rate limit window. A date for a known end time, such as a maintenance window. Both are legal in every response that may carry the field, so a client must read both.

Does curl honour retry-after?

Yes, with --retry. In step 5 curl waited 19.043 s for a header that said 19 and then repeated the request. Cap it with --retry-max-time, otherwise a large value parks the command for as long as the server asked.

What should a client do when retry-after is missing?

Fall back to exponential backoff with jitter, and record the gap. A 429 or 503 with no wait instruction leaves every client guessing, and the ones that guess badly are the reason the service is refusing traffic.

Can retry-after appear on a redirect?

Yes. RFC 9110 allows it on any 3xx response, where it means the minimum time to wait before issuing the redirected request. It is rare in practice, so check how your HTTP client behaves before relying on it.

Verified

Verified by Maks Vernycurl 8.21.0node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate5 minpublished updated Maks Verny