How to test API timeout handling
Give the request a deadline with curl --max-time 2 https://api.example.com/slow. When the server is slower than the deadline, curl stops and exits 28. Set --connect-timeout separately, because a refused port exits 7, and a connect timeout set too low reports that refusal as a timeout instead.
Why check this
Timeout handling is the code path nobody runs until production. Test it on staging before release, and again whenever a dependency is added, because every new upstream call inherits the client's default deadline. The failure it prevents is concrete: an order service waits on a payment gateway with no deadline, the gateway stalls, and the request pool fills with threads that will never finish while healthy traffic queues behind them.
Prerequisites
- curl 7.0 or later.
--max-timeand--connect-timeoutare both in the curl manual. - A slow endpoint.
https://httpbin.org/delay/5holds the response for five seconds, andhttps://httpbin.org/dripstreams a body slowly. - A TCP port on your own machine with nothing bound to it, for the connect side. The steps below use
127.0.0.1:9999. - The client under test, so you can compare what curl sees with what your code does with it.
Steps
- Step 1.
Measure what the slow endpoint actually costs, with a deadline generous enough not to fire.
curl -s -o /dev/null --max-time 10 -w 'code %{http_code} exit %{exitcode} ttfb %{time_starttransfer} total %{time_total}\n' https://httpbin.org/delay/5code 200 exit 0 ttfb 5.666032 total 5.666130The first byte arrived after 5.67 s. Every deadline below that figure has to fail, and every deadline above it has to pass.
- Step 2.
Set the deadline under the server's own time and read the exit code.
curl -s -o /dev/null --max-time 2 -w 'exit %{exitcode} total %{time_total}\n' https://httpbin.org/delay/5exit 28 total 2.014731curl gave up at 2.01 s with exit code 28, and the shell received 28 as well. There is no status code, because no response line ever arrived.
- Step 3.
Time out in the middle of a body that has already started, and count what was written.
curl -s --max-time 3 -o drip.txt -w 'exit %{exitcode} code %{http_code} received %{size_download} bytes ttfb %{time_starttransfer} total %{time_total}\n' 'https://httpbin.org/drip?duration=10&numbytes=60'exit 28 code 200 received 15 bytes ttfb 0.621193 total 3.014566This is the dangerous shape. The status is 200, the headers are valid, and
drip.txtholds 15 of the 60 bytes. A client that checks only the status code stores a truncated document and reports success. - Step 4.
Point the same command at a closed port, with a connect timeout longer than the refusal takes.
curl -s -o /dev/null --connect-timeout 5 -w 'exit %{exitcode} total %{time_total}\n' http://127.0.0.1:9999/exit 7 total 2.024551Exit 7 means the connection was refused, so nothing is listening. The 2.02 s is the operating system retrying the handshake before it gives up, not the server thinking.
- Step 5.
Repeat with a connect timeout shorter than that refusal and watch the diagnosis change.
curl -s -o /dev/null --connect-timeout 1 -w 'exit %{exitcode} total %{time_total}\n' http://127.0.0.1:9999/exit 28 total 1.000999Same dead port, same machine, different verdict. A one second connect timeout turns a refused connection into a timeout, and a tester reading exit 28 goes looking for a slow network instead of a stopped service.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| exit 0, total under the deadline | The endpoint answered in time | Record the figure as the baseline the deadline is set against |
| exit 28, total equals --max-time | The whole request ran out of time | Check whether the client retries, and whether the server finished the work anyway |
| exit 28, total equals --connect-timeout | The connect phase ran out of time | Raise the connect timeout and rerun before blaming the network |
| exit 7, total below the connect timeout | The port refused the connection | The service is down or bound to another port. No timeout is involved |
| exit 28 with http_code 200 | Headers arrived, the body did not finish | Confirm the client discards the partial body instead of storing it |
Common mistakes
What to check next
- How to check API response time: the measurement that tells you where to put the deadline.
- How to test API error responses: what the client should return to its own caller once the deadline fires.
- How to test retry-after header: the server's instruction for when a retry is welcome.
- How to test API concurrency: timeouts and retries together are how one slow dependency becomes duplicate writes.
FAQ
How to set a timeout for a curl request?
--max-time caps the entire transfer in seconds, and --connect-timeout caps the connect phase only. Set both. Fractional values work, so --max-time 2.5 is valid. Without either flag curl waits for as long as the server keeps the socket open.
What is curl exit code 28?
The operation timed out. It covers the connect timeout, the total deadline and the low speed limit, so the number alone does not say which one fired. Compare %{time_total} with the deadlines you set to tell them apart.
How to check API timeout behaviour without a slow endpoint?
Bind a local port to a script that sleeps before answering, or use https://httpbin.org/delay/5. Do not send repeated slow requests to a service you do not own, because a held connection costs the other side a worker.
Should a timed-out request be retried?
Only when the operation is idempotent or carries an idempotency key. A timeout does not tell the client whether the server applied the change, so a blind retry on a create can produce two records.
Does a timeout roll back the server's work?
No. The server usually never learns that the client left. Verify the effect by reading the resource after the timeout rather than assuming the transaction was cancelled.
Verified
Verified by Maks Vernycurl 8.21.0
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
intermediate6 minpublished updated Maks Verny