A quick disclosure before the commands: we are Geonode and we sell proxies, which means the honest thing to say up front is that a curl timeout is almost never a proxy problem. If your requests are timing out against a server that is simply slow, or a host that is down, or a firewall that drops packets silently, routing them through us changes nothing except the bill. Proxies help when the issue is where your request appears to come from — geo-restricted content, rate limits keyed to your address, an IP that has been blocked. They do not make a slow server fast. Set your timeouts correctly first; you may find you never needed anything else.
Right. Here is the thing most people discover the hard way: curl has no default overall timeout. It has a default connect timeout of 300 seconds, but once a connection is established, curl will happily wait indefinitely for a response that never fully arrives. In a shell script running in cron, that is a job that never finishes and a lock file nobody clears.
The Two Timeouts That Matter
Everything else is a refinement of these two.
--max-time (short form -m) caps the entire operation. From the curl manual: "Maximum time in seconds that you allow the transfer operation to take." Connection, handshake, request, response, all of it. When the limit is hit, curl aborts and exits with code 28.
curl -m 10 https://example.com
Ten seconds total, then curl gives up regardless of what stage it reached.
--connect-timeout caps only the setup phase. The manual is precise about what that includes: "The connection phase is considered complete when the DNS lookup and requested TCP, TLS or QUIC handshakes are done." Once the connection is up, this option stops applying.
curl --connect-timeout 3 https://example.com
Three seconds to resolve the name and complete the handshakes. After that, curl waits as long as the transfer takes.
In practice you want both, and they answer different questions:
| Option | Covers | Typical value | What it protects against |
|---|
--connect-timeout | DNS + TCP/TLS/QUIC handshake | 3–10s | Dead hosts, dropped packets, DNS failures |
--max-time | The whole operation | 10–60s, workload dependent | Slow responses, stalled transfers, endless streams |
Together:
curl --connect-timeout 5 -m 30 https://example.com
Five seconds to get connected, thirty seconds for the whole thing. If the host is unreachable you fail in five rather than thirty, which matters a great deal when you are iterating over a list of a thousand URLs.
Choosing Values That Are Not Guesses
The usual approach is to pick a round number and adjust it upward whenever something fails. That converges on a value large enough that the timeout no longer does anything useful.
A better method takes one extra command. curl can report where the time actually went:
curl -o /dev/null -s -w "dns: %{time_namelookup}\nconnect: %{time_connect}\ntls: %{time_appconnect}\nttfb: %{time_starttransfer}\ntotal: %{time_total}\n" https://example.com
Run that against your real target twenty or thirty times and you have a distribution rather than a guess. Then:
Set --connect-timeout from time_appconnect. Take the p95 and roughly double it. Connection time is dominated by network round trips and is fairly stable; if it takes twice as long as usual, something is genuinely wrong rather than merely slow.
Set --max-time from time_total. Here the multiplier should be more generous — three to five times the p95 — because total time depends on response size and server load, both of which vary legitimately. A tight --max-time produces flaky scripts that fail on ordinary bad days.
Two workload-specific adjustments. If you are downloading large files, --max-time is the wrong tool entirely, because a legitimate download can exceed any reasonable fixed limit — use the speed-based option below instead. And if you are calling an API that does real work server-side, ask what its own timeout is and set yours slightly higher; timing out at 10 seconds against a service that returns at 12 means you pay for the work and discard the result.
Fractional Seconds and Millisecond Precision
Both options accept decimals, using a dot as the separator regardless of your locale. This has been supported since curl 7.32.0.
curl --connect-timeout 0.5 -m 2.5 https://example.com
Half a second to connect, two and a half seconds total. Useful for health checks and for any loop where a full second of waiting per failure adds up.
One caveat worth knowing: precision degrades as the value grows. The curl documentation notes that the actual timeout decreases in accuracy as the specified timeout increases in decimal precision. Writing --max-time 30.001 is not meaningfully different from --max-time 30. Decimals are for sub-second values; above a few seconds, use whole numbers.
The related --expect100-timeout also takes decimals. It controls how long curl waits for a 100 Continue before sending a request body, defaulting to one second. If you are POSTing large bodies to a server that never sends 100 Continue, you are paying that second on every request — either lower it or disable the expectation with -H "Expect:".
Catching Stalled Transfers That Never Time Out
Here is the gap. A transfer that delivers one byte every few seconds is never idle, so no connection-level timeout fires, and if --max-time is set high enough for legitimate large downloads it will not fire either. The request just crawls.
--speed-limit and --speed-time handle this. From the manual: "If a download is slower than --speed-limit bytes per second during a --speed-time period, the transfer gets aborted."
curl --speed-limit 1000 --speed-time 30 -O https://example.com/large-file.zip
If throughput stays below 1000 bytes per second for 30 continuous seconds, curl aborts — again with exit code 28. A download that runs for six hours at a healthy rate is untouched. A download that stalls is killed in thirty seconds.
This is the correct timeout for anything of unpredictable size, and it composes well:
curl --connect-timeout 5 --speed-limit 1000 --speed-time 30 -O https://example.com/large-file.zip
Fast failure on a dead host, no overall cap, but a stall detector throughout. For file downloads specifically this pattern belongs in every script — see our guide on downloading files with curl for the surrounding options.
Timeouts and Retries Interact Badly by Default
This is the part that catches people out.
--retry N makes curl retry transient errors up to N times. What is easy to miss is that --max-time applies to each attempt, not to the command as a whole, and that curl waits between attempts with an exponential backoff starting at one second and doubling.
So this:
curl -m 10 --retry 5 https://example.com
can legitimately take well over a minute: five attempts of up to ten seconds each, plus backoff waits of 1, 2, 4, 8 and 16 seconds. If you wrote it expecting a ten-second ceiling, you were wrong by a factor of six.
--retry-max-time is the fix. It caps the total time spent retrying:
curl -m 10 --retry 5 --retry-max-time 40 https://example.com
Now curl stops starting new attempts once 40 seconds have elapsed. Note the wording — it will not start a new retry after the limit, but an attempt already in flight runs to its own --max-time. The genuine worst case is --retry-max-time plus one --max-time.
--retry-delay replaces the exponential backoff with a fixed wait, which makes total runtime predictable:
curl -m 10 --retry 3 --retry-delay 2 --retry-max-time 40 https://example.com
One more flag worth knowing: by default --retry only fires on a narrow set of transient conditions. --retry-all-errors broadens it considerably — the manual describes it as retrying "all transient errors including FTP 4xx and 5xx response codes". It is genuinely useful in flaky-network scripts and genuinely dangerous in front of a non-idempotent POST. Think before adding it.
Timeouts When You Are Going Through a Proxy
Add -x and the timing changes shape, because there are now two connections rather than one: you to the proxy, and the proxy to the target.
curl -x http://user:pass@proxy.example.com:9000 --connect-timeout 10 -m 45 https://example.com
Three things behave differently from the direct case.
--connect-timeout measures your hop to the proxy, not the proxy's hop to the target. For HTTPS, curl issues a CONNECT and the proxy establishes the onward connection; the time that takes counts against --max-time, not --connect-timeout. A short connect timeout will therefore not protect you against a proxy that accepts your connection promptly and then takes twenty seconds to reach the target.
Residential proxies are slower, legitimately. Traffic exits through a real consumer connection, so an extra few hundred milliseconds is normal rather than a fault. Timeout values tuned for a direct connection will produce failures that look like broken proxies and are actually just physics. Measure through the proxy with the -w command above and set values from that measurement, not from your direct-connection numbers.
Failures are ambiguous. Exit code 28 through a proxy could mean the proxy is slow, the target is slow, or the target is deliberately stalling your request. Distinguishing them requires testing the components separately, which is a large enough subject that we wrote a separate guide on testing proxies.
A practical pattern for proxied requests: a generous --connect-timeout (10s), a --max-time set from measured behaviour rather than hope, and no --retry-all-errors, because through a proxy a 5xx frequently means the target is refusing you rather than having a bad moment, and retrying makes that worse.
Reading the Exit Code
curl's exit code tells you which stage failed, which is more information than most scripts bother to use.
| Code | Name | Meaning |
|---|
| 6 | CURLE_COULDNT_RESOLVE_HOST | DNS failed — no timeout involved |
| 7 | CURLE_COULDNT_CONNECT | "Failed to connect() to host or proxy" |
| 28 | CURLE_OPERATION_TIMEDOUT | "The specified time-out period was reached" |
| 56 | CURLE_RECV_ERROR | Failure receiving network data — connection dropped mid-transfer |
Descriptions are from the libcurl error reference.
The distinction between 7 and 28 is the useful one. Code 7 means the connection was actively refused — something answered and said no, quickly. Code 28 means nothing answered in time. The first usually indicates a wrong port or a closed service; the second indicates a dropped packet, a silently filtering firewall, or a genuinely overloaded host. Retrying is reasonable for 28 and usually pointless for 7.
In a script:
curl --connect-timeout 5 -m 30 -sS https://example.com > out.txt
case $? in
0) echo "ok" ;;
6) echo "dns failure" ;;
7) echo "connection refused" ;;
28) echo "timed out" ;;
*) echo "other failure" ;;
esac
Note that all three timeout mechanisms — --max-time, --connect-timeout and the speed limit pair — return 28. The exit code tells you a limit was reached, not which one. If you need to know, use -w "%{time_total}" and compare against your configured values.
When Not to Set a Timeout
Against the general advice, there are cases where a timeout is the wrong instrument.
Interactive downloads. If you are typing a curl command at a terminal to fetch a large file, you are the timeout. You can see the progress bar and press Ctrl-C. Adding -m here only produces the annoyance of a download that dies at 90%.
Long-lived streams. Server-sent events, log tailing, chunked responses that stay open by design — --max-time will terminate these at exactly the wrong moment. Use --speed-limit and --speed-time if you need a stall detector, or nothing at all.
Anything already wrapped in an external limit. If curl runs under timeout(1), a systemd unit with RuntimeMaxSec, or a CI step with its own budget, a second layer adds nothing but a second number to keep in sync. Pick the layer that produces the error message you want to read and set it there.
As a fix for a slow target. A timeout makes a slow request fail faster. It does not make it succeed. If your real problem is that a server takes 40 seconds to respond, the options are caching, pagination, a different endpoint, or a conversation with whoever runs it — and this is where we will repeat the disclaimer from the top, since it is the most common thing people buy proxies to solve and one of the few things proxies cannot solve at all. Our pricing starts at $0.79/GB for residential traffic and $0.14/GB for datacentre, checked as of September 2026, and none of it will speed up a slow origin server.
People Also Ask
What is the default timeout in curl?
There is no default overall timeout — curl waits indefinitely for a response once connected. The connect phase does have a default of 300 seconds. This is why -m matters in scripts: without it, a hung request hangs the script.
What is the difference between --max-time and --connect-timeout?
--connect-timeout covers only DNS resolution and the TCP/TLS/QUIC handshakes, and stops applying once the connection is established. --max-time covers the entire operation from start to finish, including the response transfer. Use both — a short connect timeout for fast failure on dead hosts, a longer max time for the overall ceiling.
Why does my curl command take longer than the timeout I set?
Almost always retries. --max-time applies per attempt, and --retry adds both extra attempts and exponential backoff between them. Add --retry-max-time to cap the total, and remember the worst case is that value plus one more --max-time.
How do I set a curl timeout in milliseconds?
Use a decimal value: --connect-timeout 0.25 is 250 milliseconds. Both --max-time and --connect-timeout accept decimals with a dot separator, supported since curl 7.32.0. Precision is best below a few seconds; for larger values the accuracy of the fractional part degrades.
What exit code does curl return on a timeout?
28, CURLE_OPERATION_TIMEDOUT. All three timeout mechanisms return it, so the code tells you a limit was hit but not which one. Compare against 7 (connection refused) and 6 (DNS failure), which mean something different and usually should not be retried.
How do I time out a slow download without killing large files?
Use --speed-limit and --speed-time instead of --max-time. They abort only when throughput stays below a threshold for a sustained period, so a legitimate multi-hour download is unaffected while a stalled one dies quickly.
Do timeouts work differently through a proxy?
Yes. --connect-timeout measures your connection to the proxy only; the proxy's onward connection to the target counts against --max-time. Residential proxies also add genuine latency, so values tuned for direct connections will produce false failures. Measure through the proxy and set values from that.
Can I set a timeout for DNS resolution alone?
Not as a separate option — DNS is included in --connect-timeout. If you need DNS specifically bounded, resolve separately and pass the result with --resolve, which skips curl's own lookup entirely.
Wrapping Up
Two options cover almost every case: --connect-timeout for fast failure when a host is unreachable, --max-time for the overall ceiling. Set both, in every script, always. The default of waiting forever is a reasonable choice for an interactive tool and a terrible one for automation.
The two things that trip people up are worth repeating. --max-time is per attempt, so any use of --retry needs --retry-max-time alongside it or your ten-second command becomes a one-minute command. And a fixed time limit is the wrong tool for transfers of unpredictable size — use --speed-limit with --speed-time and you get a stall detector that leaves legitimate long downloads alone.
Set the values from measurement rather than from a round number that felt right. One run of curl -w against your actual target gives you a distribution, and a timeout derived from a distribution fails when something is genuinely wrong rather than whenever the network has an ordinary bad afternoon.