Technology · PRACTICAL GUIDE
curl error 28: measure DNS, connect, TLS and first-byte time before raising limits
Error 28 says a timeout condition was reached, not which layer waited. Capture curl’s phase timings, compare one controlled variant, and change the timeout only after the slow phase is proven.

A curl 28 result is a budget alarm. It can occur before a TCP connection exists, during a TLS handshake, while waiting for the first response byte, or after data starts moving. The same exit code therefore needs different repairs. Preserve the failing command and environment, measure the cumulative phases, then change one variable at a time.
Freeze the reproduction before editing limits
Record the exact URL without secrets, HTTP method, runner or host, proxy settings, DNS path, IP family and the output of curl -V. Run the same request from the failing environment and keep the exit code. If one environment succeeds while another fails, that is evidence of an environment boundary—not proof that the remote service is healthy for every caller.
Emit a phase ledger with --write-out
Use a harmless request and write out time_namelookup, time_connect, time_appconnect, time_starttransfer and time_total. These are cumulative timestamps from the start. Compare adjacent values: connect minus name lookup approximates the TCP phase; appconnect minus connect isolates the TLS or SSH handshake; starttransfer minus pretransfer highlights server wait; total minus starttransfer shows response transfer time.
Classify the first phase that never completes
A name-lookup stall points toward resolver reachability or policy. A large or missing TCP-connect result points toward route, address family, firewall, proxy or listener reachability. A TCP connection followed by a TLS stall points toward handshake, trust path, inspection or protocol negotiation. A completed handshake followed by a long first-byte time points toward the server, its upstreams or an intermediary. A late total-time failure with bytes received points toward throughput or a stalled body.
Test one controlled variant
Keep the method and destination stable. Compare the same command from a known-working host; then, when policy permits, test IPv4 versus IPv6, with and without the configured proxy, or a resolved destination supplied through --resolve. Do not send production credentials to a substitute host. Each comparison should answer one question and leave an auditable before/after timing record.
Separate connection and whole-transfer budgets
--connect-timeout limits the connection phase, including the protocol handshakes documented by curl. --max-time caps each complete transfer attempt; retries can reset that per-attempt counter, while --retry-max-time limits the retry window. Keep these budgets explicit. Raising --max-time alone is justified only when the measured slow phase is expected and the calling job can safely wait longer.
Prove the fix and keep rollback simple
Repeat the original command enough times to distinguish a fix from a transient success. Preserve phase timings, effective URL, response code, remote IP when available, exit code and the single configuration change. If the wait remains unexplained, restore the prior setting and escalate with the ledger instead of stacking retries, disabling certificate checks or removing network controls.
