How to test retries, backoff and timeouts in an HTTP client
Create a mock endpoint that returns 429 with Retry-After, 503, or a slow reply, then confirm your client backs off, gives up, and times out the way you expect.
Retry code is rarely exercised before production
Most HTTP clients have retry logic that has never run. Real APIs rarely rate-limit or fail on demand in development, so the code that reads Retry-After, waits, and tries again is first tested by an outage. Bugs there tend to make an outage worse: clients that retry instantly, retry forever, or retry requests that were never safe to repeat.
A test endpoint that fails the same way every time makes this code testable. socket.help mock endpoints can return a chosen status, delay a reply by up to two seconds, and enforce a per-route quota that answers with 429 and a Retry-After header once it is used up.
Decide what correct behavior looks like
Write down the expected behavior before you build the mock, so the test has something to assert. A useful set of cases covers a rate limit, a temporary server failure, a permanent client error, and a slow response.
- On 429, wait at least as long as Retry-After says, then retry.
- On 503, retry a small number of times with increasing delays and some random jitter.
- On 400, 401, 404, or 422, do not retry; report the error.
- When a response takes longer than your timeout, cancel the request and treat it as a failure.
- Only retry POST and other non-idempotent requests when you use an idempotency key or know a repeat is harmless.
Create one mock with a route for each case
Use the Rate-limit simulator, Fault injection, or Delay and timeout simulator pages, or create a mock API directly. They all build the same kind of resource. Each route has a method, path, status, body, and optional delayMs; requestsPerMinute sets a quota from 1 to 120 for that route.
The example is public so that the test client needs no header. If you choose private, send the returned access token as X-Access-Token on every request.
curl -X POST 'https://socket.help/api/mocks?visibility=public' \
-H 'Content-Type: application/json' \
-d '{
"name": "Retry tests",
"routes": [
{ "method": "GET", "path": "/limited", "status": 200, "body": "{\"ok\":true}", "requestsPerMinute": 2 },
{ "method": "GET", "path": "/unavailable", "status": 503, "body": "{\"error\":\"Try again later\"}" },
{ "method": "POST", "path": "/invalid", "status": 422, "body": "{\"error\":\"Missing email\"}" },
{ "method": "GET", "path": "/slow", "status": 200, "body": "{\"ready\":true}", "delayMs": 2000 }
]
}'How the rate-limit route behaves
The quota uses a fixed one-minute window that starts with the first counted request. With requestsPerMinute set to 2, the first two requests get the configured response and the third gets 429 with the body {"error":"Configured mock quota exceeded."}. Retry-After is a whole number of seconds until the window resets, at least 1.
RFC 9110 allows Retry-After to be either a number of seconds or an HTTP date. This mock always sends seconds, but a production API may send a date, so make sure your parser handles both. Because the window is shared by every caller of that route, run tests that depend on it one at a time.
for i in 1 2 3; do
curl -s -o /dev/null -D - https://socket.help/mock/MOCK_ID/limited \
| grep -i -E '^HTTP/|^retry-after'
doneTest timeouts with a client deadline below the delay
The /slow route waits two seconds before replying, which is the maximum delay. To test a timeout, set your client’s deadline below that, for example one second, and check that the request is cancelled, the error is reported, and any loading state ends. Then raise the deadline above two seconds and confirm the same request succeeds.
A delay starts after the request has been accepted, so it simulates a slow server, not a slow or dropped connection. Connection refusals, DNS failures, packet loss, and responses that stall halfway through are out of scope. Test those with a tool that controls the network, such as a proxy in your test environment.
const response = await fetch('https://socket.help/mock/MOCK_ID/slow', {
signal: AbortSignal.timeout(1000),
});
// Expect a TimeoutError to be thrown before a response arrives.Assert on counts and timing, not just the final result
A retry test that only checks the final outcome can pass while the client hammers the server. Count how many requests your client made, and record the time between them. On /unavailable, the count should stop at your retry limit and the gaps should grow. On /invalid, there should be exactly one request. On /limited, the retry after a 429 should not happen before Retry-After has passed.
Mock endpoints accept 120 requests per minute in total, on top of any route quota, and also answer 429 with Retry-After when that limit is reached. Keep retry loops bounded so a bug in the test does not lock you out for the rest of the minute.
Limits and cleanup
A mock API supports up to 20 routes in 80 KiB of configuration and starts with a one-hour lifetime. Responses are static: the mock cannot fail the first two calls and then succeed, apart from what a quota produces. To test a recovery sequence, combine a quota route with your own retry count, or update the configuration between runs with a PUT to /api/mocks/MOCK_ID using the management token.
Delete the mock when you are done with a DELETE to the same path. If your tests need to run in CI on every commit, keep the configuration in your repository and create a fresh mock at the start of each run. Without an account you can create up to 10 resources per minute and hold five at a time, so delete each mock at the end of the run.
Questions people ask
How do I return a 429 with Retry-After?
Add requestsPerMinute to a mock route. Once the quota for the current one-minute window is used, the route returns 429 with Retry-After in seconds.
What is the longest delay I can configure?
Two seconds per route. Set your client timeout below that to test the timeout path.
Can the mock simulate a dropped connection?
No. It returns HTTP responses with chosen statuses and delays. Connection-level failures need a network or proxy tool.
Should POST requests be retried?
Only when a repeat is safe, for example with an idempotency key. Otherwise a retry can create a duplicate.