In PHP's cURL extension, how do CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT differ, and what happens when neither is set?
answer
- two phases of one request
- connect phase versus whole transfer
- defaults of 300 and 0
- CURLE_OPERATION_TIMEDOUT from curl_errno
- max_execution_time and network waits
basics
~20 sCURLOPT_CONNECTTIMEOUT caps only connection setup (default 300 seconds); CURLOPT_TIMEOUT caps the whole transfer (default 0, meaning never). Without both, a hanging API can hold a PHP worker for minutes; set a short connect timeout and a total timeout.
solid answer
~40 s`CURLOPT_CONNECTTIMEOUT` limits how long cURL waits to establish the connection. The manual gives its default as 300 seconds. `CURLOPT_TIMEOUT` limits the **entire** operation, connect included. Its default is `0`, which means a transfer never times out. So with neither set, a shipping-rates API that accepts the connection and then stalls can block the request forever. The worker stays busy, requests queue behind it, and `max_execution_time` does not reliably rescue you: on the usual non-thread-safe Linux build it counts script execution, not time spent waiting in network calls. I set both, for example `CURLOPT_CONNECTTIMEOUT => 2` and `CURLOPT_TIMEOUT => 5`, or the `_MS` variants. On expiry `curl_exec()` returns `false` and `curl_errno()` is `CURLE_OPERATION_TIMEDOUT`, which I map to a fallback such as a cached rate.
code
php · 22 lines<?php
declare(strict_types=1);
function fetchRates(string $url): ?array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2, // connection setup only
CURLOPT_TIMEOUT => 5, // whole transfer, connect included
]);
$body = curl_exec($ch);
if ($body === false) {
if (curl_errno($ch) === CURLE_OPERATION_TIMEDOUT) {
return null; // caller falls back to a cached rate
}
throw new RuntimeException(curl_error($ch));
}
return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}go deeper
Remember the two options and their split: one caps connecting, the other caps the whole request. Know that the total timeout defaults to 0, meaning no limit.
Explain why a hanging upstream blocks a worker, why max_execution_time does not count network waits on a typical Linux FPM build, and how to detect CURLE_OPERATION_TIMEDOUT and fall back.
Derive the values from the caller's latency budget and the pool size, use curl_getinfo timings to tune them, and watch for sub-second values with the system resolver.
Treat every outbound call as a budgeted dependency: agree on timeouts per upstream, and prefer a degraded answer over a stuck worker pool when a partner API slows down.
## Two phases, two limits An HTTP call made through PHP's **cURL extension** has distinct phases: name resolution, the TCP connect, the TLS handshake for `https`, sending the request, and then waiting for and reading the response. libcurl exposes two main limits over them: | Option | What it limits | Default (manual) | Millisecond variant | |---|---|---|---| | `CURLOPT_CONNECTTIMEOUT` | time to establish the connection | `300` seconds; `0` means wait indefinitely | `CURLOPT_CONNECTTIMEOUT_MS` | | `CURLOPT_TIMEOUT` | the whole operation, connect included | `0`, meaning functions never time out | `CURLOPT_TIMEOUT_MS` | Because `CURLOPT_TIMEOUT` covers everything, it should be **at least** the connect timeout. Setting it lower means the connect limit never gets a chance to fire. ## Why the defaults are dangerous Consider a shipping-rates API that sometimes hangs: it accepts TCP connections, then takes minutes to answer, or never does. With no options set: - the connect succeeds quickly, so the 300-second connect limit is irrelevant; - `CURLOPT_TIMEOUT` is `0`, so nothing stops the wait for the response; - the PHP worker serving the checkout page is blocked inside `curl_exec()`. Under PHP-FPM every worker is a separate process with a fixed pool size. A handful of hung calls can occupy every worker, so unrelated pages queue and the site looks down. A fast failure is far cheaper than a slow one. ## A worked budget Suppose the checkout page should render in about three seconds, and the shipping-rates lookup is one of several steps. A reasonable split looks like this: - **connect timeout: 1-2 seconds.** If the TCP and TLS setup has not finished by then, the host is unreachable or overloaded, and waiting longer rarely helps. - **total timeout: 2-3 seconds.** This bounds the whole call, so the worst case for the step is known in advance. - **fallback: a cached or flat rate.** The page still renders, and the log records that the upstream was slow. The arithmetic also applies to capacity. If an FPM pool has 20 workers and a hung upstream holds each affected request for 60 seconds, 20 checkouts in a minute are enough to fill the pool. With a 3-second cap, the same pool keeps serving other pages while the upstream recovers. ## Why max_execution_time does not save you It is tempting to rely on `max_execution_time`. The manual's note on `set_time_limit()` is explicit: the limit counts only the execution time of the script itself. Time spent outside it, such as system calls, stream operations and database queries, is not included, and network waits inside `curl_exec()` fall in the same category. The exception the manual names is Windows, where the measured time is real (wall-clock) time. In the php-src source, thread-safe Linux builds also use a wall-clock timer, but a typical non-thread-safe PHP-FPM build on Linux measures CPU time. On such a server a script can sit in `curl_exec()` far longer than `max_execution_time` without being stopped. The web server's or FPM's own request timeouts are a last resort, and they kill the request rather than letting the code degrade gracefully. ## Picking values and reacting to expiry 1. Choose the **connect timeout** from the network: a healthy connection inside a data centre or to a nearby region is set up in milliseconds, so 1-3 seconds is generous. 2. Choose the **total timeout** from the caller's budget: if checkout must render within a few seconds, the rates call cannot have more than part of that. 3. When a timer expires, `curl_exec()` returns `false`. `curl_errno()` returns `CURLE_OPERATION_TIMEDOUT` (the older alias `CURLE_OPERATION_TIMEOUTED` still exists) and `curl_error()` says how long it waited. 4. Degrade deliberately: show a cached or flat rate, or tell the user to retry. Whether and how to retry is a policy decision, not a cURL option. `curl_getinfo()` helps tune the numbers. `CURLINFO_CONNECT_TIME`, `CURLINFO_STARTTRANSFER_TIME` and `CURLINFO_TOTAL_TIME` (array keys `connect_time`, `starttransfer_time`, `total_time`) show where a slow call spent its time. ## Edge cases worth knowing - **Sub-second values.** `CURLOPT_TIMEOUT_MS` and `CURLOPT_CONNECTTIMEOUT_MS` accept milliseconds. The manual warns that if libcurl uses the standard system name resolver, the DNS part of the connect still has one-second resolution with a minimum of one second. - **Signals.** With the system resolver, libcurl can use signals to time out DNS lookups. `CURLOPT_NOSIGNAL` disables that, and PHP turns it on by default only in thread-safe builds. - **Slow drip.** A server that sends a byte every few seconds never trips an idle detector, but `CURLOPT_TIMEOUT` still ends it. `CURLOPT_LOW_SPEED_LIMIT` with `CURLOPT_LOW_SPEED_TIME` aborts transfers that stay below a speed for too long. - **Not the socket default.** The `default_socket_timeout` ini setting governs PHP's stream functions, not cURL; a cURL handle uses only its own options.
- The rates API connects in 20 ms but then never answers. Which option ends the wait, and which never fires?`CURLOPT_TIMEOUT` ends it, because it bounds the whole operation. `CURLOPT_CONNECTTIMEOUT` never fires: the connection was established in 20 ms, and that option only limits the connect phase. With `CURLOPT_TIMEOUT` left at its default of `0`, the call would wait indefinitely.
- How do you find out which phase of a slow cURL call took the time?Read the timings from `curl_getinfo()`: `CURLINFO_NAMELOOKUP_TIME`, `CURLINFO_CONNECT_TIME`, `CURLINFO_STARTTRANSFER_TIME` and `CURLINFO_TOTAL_TIME`, or the array keys `namelookup_time`, `connect_time`, `starttransfer_time` and `total_time`. A large gap before the first byte points at the server; a large connect time points at the network or DNS.
saying these in an interview costs you the question
- CURLOPT_TIMEOUT only limits the time to connect.
- cURL requests time out after 30 seconds by default.
- max_execution_time on a typical Linux FPM build will stop a script stuck in curl_exec().
- default_socket_timeout also applies to cURL handles.
- Setting CURLOPT_TIMEOUT to 0 makes the request fail immediately.