Debug HTTP 503 with Retry-After Backoff
Reproduce a 503 response, read the Retry-After header, and prove a client backoff schedule respects server guidance under load.
Task
Capture a 503 response together with its Retry-After header, then script a retry schedule that honours the server-provided delay.
When to use this
A partner API returns 503 during peak hours but the client retries immediately, amplifying the outage instead of easing it.
Prerequisites
- curl 8.x and jq installed
- A staging endpoint that can be forced to return 503
- A client retry loop you can modify
Steps
1. Reproduce the failure and record the status line plus response headers.
curl -sS -D - -o /dev/null -w 'status=%{http_code}\n' \
-H 'Accept: application/json' \
https://api.staging.example.com/v1/reportsExpected output: The output shows status=503 and a Retry-After header expressed in seconds or as an HTTP date.
2. Read the Retry-After value and sleep exactly that long before the next attempt.
retry_after=$(curl -sSI https://api.staging.example.com/v1/reports | awk -F': ' 'tolower($1)=="retry-after"{print $2}' | tr -d '\r')
sleep "${retry_after:-5}"
echo "retrying after ${retry_after:-5}s"Expected output: The client waits the server-provided delay instead of a fixed short interval.
3. Add jittered exponential backoff that never exceeds the header value.
def backoff(attempt, retry_after):
base = min(2 ** attempt, retry_after)
return base + random.uniform(0, base * 0.2)Expected output: Attempts spread out so recovering services are not hit by a synchronized retry storm.
Troubleshooting
Problem
Retry-After is an HTTP date, not seconds
Solution
Parse it with a date library and subtract the current time instead of assuming an integer.
Problem
The client retries during declared maintenance
Solution
Treat 503 with Retry-After as authoritative and gate the loop on the header.
Problem
Retries succeed but create duplicate records
Solution
Send an idempotency key so a retried request cannot double-apply.
Limitations
- Covers HTTP-level retry signals only; application error codes need their own policy.
- Assumes the server emits Retry-After; otherwise fall back to exponential backoff.
- Does not model server queue depth, so aggressive retries can still overload a struggling service.
What this recipe does
Capture a 503 response together with its Retry-After header, then script a retry schedule that honours the server-provided delay.
A partner API returns 503 during peak hours but the client retries immediately, amplifying the outage instead of easing it.
Prerequisites
- curl 8.x and jq installed
- A staging endpoint that can be forced to return 503
- A client retry loop you can modify
Procedure
- Reproduce the failure and record the status line plus response headers.
curl -sS -D - -o /dev/null -w 'status=%{http_code}\n' \
-H 'Accept: application/json' \
https://api.staging.example.com/v1/reports
Expected result: The output shows status=503 and a Retry-After header expressed in seconds or as an HTTP date.
- Read the Retry-After value and sleep exactly that long before the next attempt.
retry_after=$(curl -sSI https://api.staging.example.com/v1/reports | awk -F': ' 'tolower($1)=="retry-after"{print $2}' | tr -d '\r')
sleep "${retry_after:-5}"
echo "retrying after ${retry_after:-5}s"
Expected result: The client waits the server-provided delay instead of a fixed short interval.
- Add jittered exponential backoff that never exceeds the header value.
def backoff(attempt, retry_after):
base = min(2 ** attempt, retry_after)
return base + random.uniform(0, base * 0.2)
Expected result: Attempts spread out so recovering services are not hit by a synchronized retry storm.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows the retry schedule matches the Retry-After value and the number of requests during a simulated outage drops.
Troubleshooting
Retry-After is an HTTP date, not seconds — Parse it with a date library and subtract the current time instead of assuming an integer.
The client retries during declared maintenance — Treat 503 with Retry-After as authoritative and gate the loop on the header.
Retries succeed but create duplicate records — Send an idempotency key so a retried request cannot double-apply.
Limitations
- Covers HTTP-level retry signals only; application error codes need their own policy.
- Assumes the server emits Retry-After; otherwise fall back to exponential backoff.
- Does not model server queue depth, so aggressive retries can still overload a struggling service.