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

Steps

  1. 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/5
    
    code 200 exit 0 ttfb 5.666032 total 5.666130

    The first byte arrived after 5.67 s. Every deadline below that figure has to fail, and every deadline above it has to pass.

  2. 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/5
    
    exit 28 total 2.014731

    curl 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.

  3. 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.014566

    This is the dangerous shape. The status is 200, the headers are valid, and drip.txt holds 15 of the 60 bytes. A client that checks only the status code stores a truncated document and reports success.

  4. 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.024551

    Exit 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.

  5. 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.000999

    Same 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

Sign: The client reports success on a request that timed out.Cause: The status line arrives before the body, so http_code is 200 while the transfer is incomplete. Step 3 stored 15 of 60 bytes under a 200. Status and completion are separate facts, and only the exit code carries the second one.
Sign: A stopped service is reported as a slow network.Cause: --connect-timeout fires before the operating system finishes refusing the connection, so exit 7 is replaced by exit 28. The same dead port gave 7 at a five second connect timeout and 28 at one second.
Sign: The deadline works with curl and the application still hangs.Cause: --max-time caps the whole transfer, while most HTTP client libraries default to a connect timeout only, with no read timeout at all. A stalled response then waits forever. Test the library with the same endpoint, not curl alone.
Sign: A timed-out write turns up in the database anyway.Cause: A timeout cancels the client's wait, not the server's work. The handler keeps running and commits after curl is gone. Read the resource after every timeout test and check whether the side effect happened.

What to check next

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.

intermediate6 minpublished updated Maks Verny