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
- Node 18 or later. See the Node http documentation.
- curl 7.0 or later. See the curl manual for
--retry. - RFC 9110 section 10.2.3 defines the field, and RFC 6585 section 4 defines the 429 status that most often carries it.
- Save this as
retry-after.js. One route refuses with the seconds form, the other with the date form.
// 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');
- Save this as
parse-retry-after.sh. Step 4 uses it to turn either format into a number of seconds.
# 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
- Run
node retry-after.jsin a second terminal and stop it with Ctrl+C afterwards.
Steps
- 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; done1 200 retry-after= 2 200 retry-after= 3 429 retry-after=19The 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.
- Step 2.
Read the whole refusal so you can see what a client receives.
curl -s -i http://127.0.0.1:8788/searchHTTP/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
Dateheader to see where the countdown starts. - Step 3.
Read the other format on a route that answers 503.
curl -s -i http://127.0.0.1:8788/maintenanceHTTP/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
Dateheader, which is two minutes here. Using the local clock instead ofDateimports any skew between the two machines. - 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 sOne 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.
- 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/searchfinal status 200 real 0m19.043scurl 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
Thresholds
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.3What to check next
- How to test API rate limiting: produce the 429 this header rides on, and test the recovery.
- How to check rate limit headers: the quota fields that sit beside it on the same response.
- How to test API timeout handling: the other place a client decides how long to wait.
- How to test API error responses: the body beside the header, and whether it names the reason.
- How to check HTTP status code: 429 and 503 are the two statuses this field belongs on.
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.
Related on this site
intermediate5 minpublished updated Maks Verny