Fix: curl: (7) Failed to connect / (6) Could not resolve host / (28) Operation timed out
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix curl errors including 'Failed to connect to host', 'Could not resolve host', 'Operation timed out', and 'SSL certificate problem'. Covers curl exit codes 6, 7, 28, 35, 56, and 60, DNS resolution, proxy settings, timeout tuning, SSL issues, retry strategies, verbose debugging, and more.
The Exit Code You’re Seeing
Personally, of all the CLI tools I reach for daily on my dev machine, curl has the most cryptic exit codes. When I hit a curl failure, the exit number is the single piece of information I check before anything else. Here is what each of the common ones actually means:
Exit code 6: DNS resolution failed:
curl: (6) Could not resolve host: api.example.comExit code 7: Connection refused or unreachable:
curl: (7) Failed to connect to api.example.com port 443 after 12 ms: Connection refusedcurl: (7) Failed to connect to api.example.com port 443 after 30021 ms: Couldn't connect to serverExit code 28: Timeout:
curl: (28) Operation timed out after 30000 milliseconds with 0 bytes receivedcurl: (28) Connection timed out after 10015 millisecondsExit code 60: SSL certificate problem:
curl: (60) SSL certificate problem: unable to get local issuer certificateExit code 35: SSL handshake failure:
curl: (35) error:0A000410:SSL routines::sslv3 alert handshake failureExit code 56: Connection reset:
curl: (56) Recv failure: Connection reset by peerEach exit code points to a different root cause. Here’s how to diagnose and fix them all.
Quick Reference Before You Dive In
If you arrived here from Google with a curl exit code, the five facts that resolve roughly 90 percent of cases:
- Run
curl -vBEFORE anything else. Verbose mode shows exactly which stage failed (DNS, TCP, TLS, transfer). Without it you are guessing. The curl exit codes documentation lists every numeric code; the curl man page covers every flag. - Exit 6 (DNS) is almost always your local resolver, not the target. Test with
nslookup api.example.comordig api.example.com. If those fail too, fix/etc/resolv.confor your DNS provider. Inside Docker, the container often has its own broken resolver; pass--dns 8.8.8.8to confirm. - Exit 7 (refused) is firewall or server-process, NOT routing. The TCP SYN reached the host but was actively refused. Test the port with
nc -zv host 443. If the port is closed locally, the server is not listening; if it is closed from outside, a firewall is blocking it. - Exit 28 (timeout) is silent packet drop, NOT a slow server. A truly slow server returns slowly; a dropped packet looks like a timeout. Common with cloud security groups, corporate firewalls, and broken IPv6 (try
curl -4). - Exit 60 (SSL) is almost always your CA bundle. Update with
apt install ca-certificates && update-ca-certificateson Debian,update-ca-truston RHEL,apk add ca-certificateson Alpine. Reach for-konly as a temporary diagnostic; never in production scripts.
The rest of this article walks through each exit code in detail, plus the failure modes most other guides skip.
How curl Actually Works, Step by Step
curl follows a predictable sequence when making a request:
- DNS resolution: translate the hostname to an IP address. If this fails, you get exit code 6.
- TCP connection: connect to the IP on the specified port. If the server is down, the port is wrong, or a firewall blocks it, you get exit code 7. If the connection takes too long, exit code 28.
- TLS handshake: if it’s HTTPS, negotiate encryption. Certificate problems give exit code 60. Handshake failures give 35.
- Data transfer: send the request and receive the response. If the connection drops mid-transfer, exit code 56.
Knowing which step failed tells you exactly where to look.
How Other HTTP CLIs Handle These Errors
curl is the default, but the alternatives have different defaults, different verbose conventions, and different retry semantics. If a curl command keeps failing in ways you cannot explain, swapping tools is sometimes faster than swapping flags.
curl vs wget. wget shares much of curl’s networking stack mental model but defaults differently. wget follows redirects by default (curl needs -L). wget retries on transient failures by default (--tries=20). wget writes to a file by default (curl writes to stdout). wget does not support sending arbitrary HTTP methods well; use curl for PUT/PATCH/DELETE. Verbose flag: wget -d. If your script keeps hitting curl exit code 28 on flaky networks, wget --tries=5 --timeout=30 often “just works” because the retry default is more forgiving.
curl vs HTTPie. HTTPie (http) is built for humans inspecting JSON APIs. Verbose flag: -v. It colorizes responses, formats JSON automatically, and defaults to following redirects. SSL verification skip is --verify=no rather than -k. The exit codes are normalized to a smaller set; connection failure is exit 4, not curl’s 7. HTTPie depends on Python’s requests, so SSL backend behavior follows Python’s certifi bundle. If you hit curl exit 60 because your system CA bundle is broken, HTTPie often works without changes because it ships its own CA list. Article: HTTPie troubleshooting.
curl vs xh. xh is a Rust HTTPie-compatible CLI with a near-identical command surface. The verbose flag is the same (-v), the JSON inference is the same, and the exit codes match HTTPie. The reason to prefer xh is speed (~10× cold-start versus HTTPie) and a static binary with no Python dependency. HTTP/2 and HTTP/3 support are both built in.
curl vs aria2. aria2c is a download accelerator, not a general HTTP client. It splits a single download across multiple TCP connections to the same server (-x 16) and supports BitTorrent, Metalink, and HTTP/FTP/SFTP. For interactive API calls it is the wrong tool. For replacing curl -O https://example.com/large.tar.gz on a slow link, it is dramatically faster. Resume support is more robust than curl’s -C - because aria2 tracks chunks individually.
HTTP/2 and HTTP/3 support
- curl: HTTP/2 since 7.43, HTTP/3 since 7.66 but only when built against an HTTP/3-capable backend (quiche, ngtcp2, msh3). Most distro packages do not include HTTP/3. Check with
curl --versionand look forHTTP3. - wget: HTTP/2 since 1.21 (via
--http2flag in some builds). No HTTP/3 support. - HTTPie / xh: HTTP/2 yes, HTTP/3 yes in recent xh, no in HTTPie.
- aria2: HTTP/2 yes, no HTTP/3.
If you need to negotiate HTTP/3 (QUIC) specifically and your curl was built without it, install a curl with quiche/ngtcp2 (Homebrew’s curl --with-quic) or use xh.
Retry semantics across tools
- curl: No retry by default. Opt in with
--retry N. Retries on a fixed list of HTTP codes (408, 429, 500, 502, 503, 504) and connection failures.--retry-all-errorsto broaden. - wget: Retries on by default with up to 20 attempts. Disable with
--tries=1. More forgiving but can mask real failures. - HTTPie / xh: No built-in retry. Wrap in a shell loop or use
--check-status+ retry logic in your script. - aria2: Per-chunk retry built in.
--max-tries,--retry-wait, automatic exponential backoff.
If you keep hitting transient curl: (28) timeouts in CI, the cheapest fix is either curl --retry 3 --retry-all-errors or switching that call to wget.
Quick Reference: curl Exit Codes
| Exit Code | Meaning | Common Cause |
|---|---|---|
| 6 | Could not resolve host | DNS misconfiguration, typo in hostname, no internet |
| 7 | Failed to connect | Server down, wrong port, firewall blocking |
| 28 | Operation timed out | Server too slow, network issues, timeout too short |
| 35 | SSL handshake error | TLS version mismatch, cipher incompatibility |
| 56 | Connection reset | Server closed connection, proxy interference |
| 60 | SSL certificate problem | Expired cert, missing CA bundle, self-signed cert |
When to Use Which Fix
The next ten sections cover each fix in detail. The table below maps your symptom to the recommended fix.
| Your symptom | Recommended fix | Why |
|---|---|---|
| Not sure where curl is failing | Fix 1: curl -v first | Verbose shows DNS / TCP / TLS / transfer stage |
(6) Could not resolve host | Fix 2: check /etc/resolv.conf, flush DNS, Docker --dns | Local resolver broken or misconfigured |
(7) Failed to connect | Fix 3: test port with nc -zv, check firewall | Server not listening or firewall blocking |
(28) Operation timed out | Fix 4: increase --max-time, try curl -4 to skip broken IPv6 | Silent packet drop or DNS race |
(60) SSL certificate problem | Fix 5: update ca-certificates, add corporate CA if needed | CA bundle missing or stale |
(35) SSL handshake failure | Fix 6: force --tlsv1.2 or --tlsv1.3, check cipher support | TLS version or cipher mismatch |
(56) Connection reset by peer | Fix 7: try --http1.1, check rate limiting / body size | Server / proxy closed connection unexpectedly |
| Behind a corporate proxy | Fix 8: set http_proxy / https_proxy or --proxy flag | curl needs to be told about the proxy |
| Server returned 301 / 302 but curl shows nothing | Fix 9: add -L to follow redirects | curl does not follow redirects by default |
| Transient failures in CI | Fix 10: --retry 3 --retry-all-errors | Built-in retry with back-off |
If multiple rows apply, pick the topmost one.
Fix 1: Enable Verbose Mode First
Before trying anything else, run your command with -v (verbose). This shows every step of the connection process and tells you exactly where it fails.
curl -v https://api.example.com/endpointThe output shows DNS resolution, TCP connection, TLS handshake, and HTTP exchange. Look for the line where it stops or errors out.
For even more detail:
curl -v --trace-time https://api.example.com/endpointThis adds timestamps so you can see where time is being spent.
If you want full binary trace output:
curl --trace trace.log --trace-time https://api.example.com/endpointMy own rule: I never debug a curl failure without -v first. The verbose output is a free, fast oracle that tells you which stage failed (DNS, TCP connect, TLS handshake, or data transfer). Skipping it almost always costs more time than running it would have.
Fix 2: DNS Resolution Failures (Exit Code 6)
You get Could not resolve host when curl cannot translate the hostname to an IP address.
Check if DNS works at all:
# Test DNS resolution directly
nslookup api.example.com
# or
dig api.example.com
# or on systems without dig
host api.example.comIf these also fail, the problem is your DNS configuration, not curl.
Check your DNS server:
# Linux
cat /etc/resolv.conf
# macOS
scutil --dns | head -20If /etc/resolv.conf is empty or points to an unreachable nameserver, fix it:
# Temporarily set Google DNS
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.confFlush the DNS cache:
# macOS
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Linux (systemd-resolved)
sudo systemd-resolve --flush-caches
# Windows
ipconfig /flushdnsCheck /etc/hosts for overrides:
grep api.example.com /etc/hostsIf the hostname is mapped to a wrong IP there, fix or remove the entry.
Docker containers often have DNS issues. The container might not inherit the host’s DNS config. Check inside the container:
docker exec -it mycontainer cat /etc/resolv.confIf it shows an unreachable nameserver, pass DNS explicitly:
docker run --dns 8.8.8.8 myimageOr in docker-compose.yml:
services:
app:
dns:
- 8.8.8.8
- 8.8.4.4Fix 3: Connection Refused (Exit Code 7)
Failed to connect means DNS resolved fine, but the TCP connection was rejected. The remote host actively refused the connection.
Check if the server is up:
# Is the port open?
nc -zv api.example.com 443
# Or with telnet
telnet api.example.com 443If the port is closed, the server process isn’t running or it’s listening on a different port.
Check if you’re hitting the right port:
# Default ports
# HTTP = 80, HTTPS = 443
# curl uses these automatically based on the URL scheme
# Explicitly specify a port
curl -v https://api.example.com:8443/endpointCheck if a firewall is blocking outbound connections:
# Test from the same machine
# If this works but your target doesn't, it's not a general firewall issue
curl -v https://google.com
# Check iptables (Linux)
sudo iptables -L -n | grep -i drop
sudo iptables -L -n | grep 443If the server is on localhost, check what’s actually listening:
# Linux
ss -tlnp | grep :8080
# macOS
lsof -i :8080
# Windows
netstat -ano | findstr :8080Related: If you’re hitting a local dev server that refuses connections, see Fix: ERR_CONNECTION_REFUSED on localhost.
Fix 4: Timeout Issues (Exit Code 28)
curl has two separate timeout mechanisms. Understanding the difference matters.
--connect-timeout: how long to wait for the TCP connection to be established. Default: 300 seconds (5 minutes).
--max-time: total time allowed for the entire operation (connect + transfer). Default: no limit.
# Wait up to 5 seconds for connection, 30 seconds total
curl --connect-timeout 5 --max-time 30 https://api.example.com/endpointIf you’re timing out, the causes are:
- Server is slow or overloaded. Increase
--max-time. - Network latency is high. Increase
--connect-timeout. - A firewall is silently dropping packets (no RST, no response — just silence). This looks like a timeout instead of “connection refused.” This is common with cloud security groups and corporate firewalls. The same issue causes SSH connection timeouts.
- DNS resolution is slow. Add
--dns-serversor resolve manually first.
Test whether it’s a DNS or connection timeout:
# Bypass DNS by providing the IP directly
# First resolve the hostname
dig +short api.example.com
# Then connect using the IP with a Host header
curl -v --connect-to api.example.com:443:93.184.216.34:443 https://api.example.com/endpointIf the IP-based request works but the hostname-based one times out, your DNS is the bottleneck.
Force IPv4 or IPv6:
Sometimes curl tries IPv6 first, times out, then falls back to IPv4, making everything slow. Force one protocol:
# Force IPv4
curl -4 https://api.example.com/endpoint
# Force IPv6
curl -6 https://api.example.com/endpointIf -4 is significantly faster, IPv6 is broken on your network. You can make this permanent:
# In ~/.curlrc
-4Fix 5: SSL Certificate Errors (Exit Code 60)
curl verifies SSL certificates by default. If verification fails, you get exit code 60.
See exactly what’s wrong with the certificate:
curl -vI https://api.example.com 2>&1 | grep -A5 "SSL certificate"Or use openssl for a detailed view:
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/nullUpdate your CA certificate bundle:
# Debian/Ubuntu
sudo apt update && sudo apt install -y ca-certificates
sudo update-ca-certificates
# RHEL/CentOS/Fedora
sudo yum install -y ca-certificates
sudo update-ca-trust
# Alpine (Docker)
apk add --no-cache ca-certificatesSpecify a CA bundle manually:
curl --cacert /etc/ssl/certs/ca-certificates.crt https://api.example.com/endpoint--insecure / -k skips verification entirely:
curl -k https://api.example.com/endpointThis disables all certificate checks. Use it only for debugging. Never use -k in scripts that download and execute code or handle sensitive data.
Corporate proxy intercepting HTTPS? Your company’s TLS inspection proxy replaces the server’s certificate with one signed by a corporate CA. You need to add that CA to your trust store. Ask IT for the root certificate, then:
sudo cp corporate-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificatesRelated: For an in-depth guide on SSL certificate issues across all tools, see Fix: SSL certificate problem: unable to get local issuer certificate.
Fix 6: SSL Handshake Failures (Exit Code 35)
The TLS handshake fails when curl and the server can’t agree on a protocol version or cipher suite.
Force a specific TLS version:
# Force TLS 1.2
curl --tlsv1.2 https://api.example.com/endpoint
# Force TLS 1.3
curl --tlsv1.3 https://api.example.com/endpointSome older servers only support TLS 1.2. Some newer servers have dropped TLS 1.2 support. Verbose mode (-v) shows which version curl is trying.
Check which ciphers the server supports:
nmap --script ssl-enum-ciphers -p 443 api.example.comSpecify a cipher manually:
curl --ciphers 'ECDHE-RSA-AES128-GCM-SHA256' https://api.example.com/endpointCheck your curl’s TLS backend:
curl --versionThis shows which SSL library curl is compiled with (OpenSSL, LibreSSL, GnuTLS, NSS, etc.). Different backends support different TLS features. If you need TLS 1.3 and your curl uses an old OpenSSL, update it.
Fix 7: Connection Reset (Exit Code 56)
Connection reset by peer means the connection was established but the remote side closed it unexpectedly during data transfer.
Common causes:
- The server crashed or restarted during the request.
- A load balancer or proxy timed out. The server took too long to respond, and an intermediate proxy killed the connection.
- The request body is too large. The server rejected it before you finished sending.
- Rate limiting. The server dropped your connection because you sent too many requests.
- Protocol mismatch. You’re sending HTTP/2 but the server expects HTTP/1.1, or vice versa.
Force HTTP/1.1:
curl --http1.1 https://api.example.com/endpointForce HTTP/2:
curl --http2 https://api.example.com/endpointSome servers or proxies mishandle HTTP/2 connection multiplexing. Falling back to HTTP/1.1 often fixes unexplained resets.
Fix 8: Proxy Configuration
If you’re behind a proxy, curl needs to know about it. If you’re not behind a proxy but have proxy environment variables set, curl sends requests to a proxy that doesn’t exist.
Set a proxy explicitly:
curl --proxy http://proxy.corp.example.com:8080 https://api.example.com/endpoint
# SOCKS5 proxy
curl --socks5 127.0.0.1:1080 https://api.example.com/endpointEnvironment variables curl respects:
# Set proxy
export http_proxy=http://proxy.corp.example.com:8080
export https_proxy=http://proxy.corp.example.com:8080
# Bypass proxy for specific hosts
export no_proxy=localhost,127.0.0.1,.internal.example.comcurl checks http_proxy, https_proxy, HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, and NO_PROXY. Note: http_proxy is lowercase by convention (uppercase is also supported).
Check if proxy variables are accidentally set:
env | grep -i proxyIf you see proxy variables you don’t need, unset them (see Fix: Environment Variable Is Undefined for more on environment variable issues):
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXYBypass proxy for a single request:
curl --noproxy '*' https://api.example.com/endpointFix 9: Follow Redirects
curl does not follow HTTP redirects by default. If the server returns a 301 or 302, curl just shows you the redirect response; it doesn’t follow it. This can look like an error when you expected content.
# Follow redirects
curl -L https://example.com/old-path
# Follow redirects with a limit (default is 50)
curl -L --max-redirs 5 https://example.com/old-pathWithout -L, you’ll get an empty response or HTML saying “Moved Permanently” and might think the server is broken.
Fix 10: Retry Failed Requests
For transient errors (server temporarily down, network blip, rate limiting), curl has built-in retry:
# Retry up to 3 times with exponential backoff
curl --retry 3 --retry-delay 2 --retry-max-time 60 https://api.example.com/endpoint--retry: number of retries.
--retry-delay: seconds between retries (doubles each time with --retry-all-errors).
--retry-max-time: maximum total time for all retries.
By default, curl only retries on transient HTTP errors (408, 429, 500, 502, 503, 504) and connection failures. To retry on all errors:
curl --retry 3 --retry-all-errors https://api.example.com/endpointHandling rate limiting (HTTP 429):
When a server returns 429 Too Many Requests, it usually includes a Retry-After header. curl’s --retry respects this header automatically. But if you need to be more deliberate:
# Check the response headers first
curl -I https://api.example.com/endpoint
# Look for:
# HTTP/2 429
# Retry-After: 30If you’re scripting, handle 429 explicitly:
response=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/endpoint)
if [ "$response" = "429" ]; then
echo "Rate limited. Waiting before retry..."
sleep 30
curl https://api.example.com/endpoint
fiLess Obvious Things That Bit Me
VPN Is Interfering
VPNs can break curl requests in multiple ways: DNS resolution goes through the VPN’s nameservers (which may not resolve external hosts), routes change so traffic goes to a different gateway, and split tunneling may not be configured.
Test with the VPN disconnected. If curl works without the VPN, the issue is VPN routing or DNS.
curl Works But the Response Is Wrong
If curl connects successfully but returns unexpected content (HTML login page, empty response, 403 Forbidden):
# Send proper headers (many APIs require these)
curl -H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "User-Agent: MyApp/1.0" \
https://api.example.com/endpointSome servers block requests without a User-Agent header. Some CDNs require specific headers or return a CAPTCHA page for bot-like requests.
Intermittent Failures with DNS Round-Robin
If the target hostname resolves to multiple IPs and only some are healthy, you’ll get intermittent failures. Check all resolved IPs:
dig +short api.example.comTest each IP directly:
curl -v --resolve api.example.com:443:93.184.216.34 https://api.example.com/endpoint
curl -v --resolve api.example.com:443:93.184.216.35 https://api.example.com/endpointIf one IP fails, the DNS record has a stale or dead entry. Report it to the service operator.
A confession: I have shipped curl -k to production exactly once in my career, in a small internal service that “only ran inside our VPC anyway.” It worked. Then someone added a sidecar that exposed the service to a CI runner across the public internet, and we silently accepted MITM-able traffic for a quarter before an audit caught it. Never use -k in scripts that handle data or download code. Fix the certificate.
curl Hangs Indefinitely
If curl appears to hang with no output and no timeout, you probably haven’t set --max-time. By default, curl waits forever for data. Always set a timeout in scripts:
curl --connect-timeout 10 --max-time 60 https://api.example.com/endpointConnection Works from One Machine but Not Another
Compare the network path:
# Check routing
traceroute api.example.com
# Check if you're resolving to the same IP
dig +short api.example.com
# Check if your outbound IP is blocked
curl https://ifconfig.meDifferent machines may use different DNS servers, have different firewall rules, or egress from different IPs. Some APIs allowlist specific source IPs.
curl in a Docker Container Fails
Docker containers have their own networking stack. Common issues:
- No DNS: The container’s
/etc/resolv.confpoints to an unreachable nameserver. Fix with--dns 8.8.8.8. - No CA certificates: Minimal images don’t include CA bundles. Install
ca-certificates. See Fix: Docker Permission Denied for other common Docker issues. - Network mode: The container might be on a bridge network that can’t reach the external host. Try
--network hostfor debugging.
# Quick test inside a container
docker run --rm alpine sh -c "apk add --no-cache curl ca-certificates && curl -v https://api.example.com"HTTP/2 Problems
Some servers or proxies mishandle HTTP/2. Symptoms include random resets, incomplete responses, or hangs. Force HTTP/1.1 as a diagnostic step:
curl --http1.1 https://api.example.com/endpointIf HTTP/1.1 works but HTTP/2 doesn’t, the issue is server-side HTTP/2 implementation. Report it to the service operator or stick with HTTP/1.1.
Outdated curl Version
Old versions of curl may lack TLS 1.3 support, HTTP/2 support, or bug fixes for specific server configurations. Check your version:
curl --versionIf you’re on an older version (pre-7.70), consider upgrading:
# Ubuntu/Debian
sudo apt update && sudo apt install -y curl
# macOS (Homebrew)
brew install curl
# The Homebrew version is separate from Apple's system curl
# Use the full path or add to PATH:
export PATH="$(brew --prefix)/opt/curl/bin:$PATH"IPv6-Only Hosts Behind Happy Eyeballs
Modern curl uses the Happy Eyeballs algorithm to race IPv4 and IPv6 connection attempts. On networks where IPv6 is advertised via DNS but not actually routable, the race can land on IPv6, time out partially, and then look like a generic connect failure rather than a clean exit 7. Force IPv4 (curl -4) as a diagnostic. If -4 succeeds and the default fails, your network is half-broken IPv6; fix the network or set --happy-eyeballs-timeout-ms 0 to skip the race.
TLS SNI Mismatch Behind Shared Hosting
If the server uses SNI (almost all modern HTTPS does) and you connect via raw IP with a Host header, curl sends the IP as the SNI hostname by default. The server picks the wrong certificate and you get a cert mismatch (exit 51 or 60). Set SNI explicitly:
curl --resolve api.example.com:443:93.184.216.34 https://api.example.com/endpoint--resolve keeps the original hostname for both Host header and SNI while pinning the IP. This is the right tool when you want to test a specific origin behind a CDN.
macOS System curl vs Homebrew curl Disagreement
macOS ships an older /usr/bin/curl that links against Apple’s Secure Transport, not OpenSSL. It honors the macOS Keychain for trust roots but ignores --cacert in some builds and lacks HTTP/3 entirely. Homebrew installs OpenSSL-backed curl at /opt/homebrew/opt/curl/bin/curl. If a curl command works on Linux but not your Mac (or vice versa), check curl --version and confirm which binary is on PATH. The fix is usually export PATH="$(brew --prefix curl)/bin:$PATH" in your shell profile.
Large File Downloads Fail Midway
For large downloads, use -C - to resume interrupted transfers:
curl -C - -O https://example.com/large-file.tar.gzIf the server doesn’t support range requests, the resume will fail. In that case, use --retry to restart from scratch on failure:
curl --retry 5 --retry-all-errors -O https://example.com/large-file.tar.gzWhat Other Tutorials Get Wrong About curl Errors
Most curl tutorials list the same fixes but frame them in ways that produce subtle bugs.
They recommend -k (insecure mode) as a fix. -k disables SSL verification entirely. It is a debugging tool, never a production solution. Tutorials that show curl -k without warning leave readers with insecure scripts that silently accept MITM-able traffic. Fix the CA bundle instead; reach for -k only to confirm whether the cert is the actual cause.
They blame “the network” for exit 28 timeouts. The most common cause of unexplained timeouts is silent packet drop, often from broken IPv6 routing (advertised via DNS but not actually routable) or from a firewall that drops packets instead of rejecting them. curl -4 and --connect-timeout 5 together expose both causes in seconds. Tutorials that just suggest raising the timeout miss this.
They mix up DNS resolution time with connection time. --connect-timeout covers TCP connection establishment, not DNS lookup. A slow resolver does not respect --connect-timeout. The fix for slow DNS is either --resolve (skip DNS) or --dns-servers (use specific resolvers), not increasing the connect timeout.
They omit Happy Eyeballs as a failure mode. Modern curl races IPv4 and IPv6 connection attempts in parallel. When IPv6 is advertised but broken, the race can land on IPv6, partially time out, and look like a generic connect failure. Tutorials that do not mention --happy-eyeballs-timeout-ms 0 or -4 as diagnostic steps leave readers stuck.
They assume curl follows redirects. It does not. Many “the server returned nothing” reports are 301 responses curl correctly showed but the user expected curl to follow. The -L flag is required; tutorials that omit it in their examples send readers chasing empty-response phantoms.
They miss the macOS system curl vs Homebrew curl divergence. Apple’s /usr/bin/curl uses Secure Transport and reads the macOS Keychain for trust roots. Homebrew’s curl uses OpenSSL with a different CA bundle. A command that works on Linux can fail on Mac (and vice versa) for reasons that look like network issues but are actually backend differences. curl --version discloses which backend is in use.
Frequently Asked Questions
What does each curl exit code mean?
The most common: 6 = DNS resolution failed, 7 = connection refused or unreachable, 28 = operation timed out, 35 = SSL handshake failed, 51 = SSL peer certificate fingerprint mismatch, 52 = empty reply from server, 56 = connection reset by peer, 60 = SSL certificate problem (CA bundle), 77 = problem reading SSL CA cert. The complete list lives in the libcurl error documentation; reading it once will repay itself many times.
Why does curl example.com work but my script with the same URL fails?
Usually one of three things: your script runs in a different shell with different environment variables (proxy env vars set or unset, different ~/.curlrc); your script uses a different curl binary (system curl vs Homebrew curl on Mac, vs a Docker image without ca-certificates); or your script captures output / redirects in a way that swallows the error message. Run the failing command interactively with -v to surface the actual cause.
When should I use -k to skip SSL verification?
Only as a one-time diagnostic to confirm whether the SSL layer is the problem. If -k works and the normal command fails, the cause is your CA bundle, the server’s certificate chain, or a corporate TLS-inspection proxy. Never use -k in scripts. The fix is to install the missing CA, update ca-certificates, or add the corporate root CA to your trust store.
What is the difference between --connect-timeout and --max-time?
--connect-timeout is the maximum time for the TCP connection to be established. --max-time is the maximum time for the entire operation, including connection, TLS handshake, request transmission, and response transfer. For most scripts you want both: --connect-timeout 5 --max-time 30. The connect timeout fails fast on unreachable hosts; max-time prevents a slow server from hanging your pipeline indefinitely.
Why does curl in a Docker container fail when it works on the host?
Three common causes. First, the container’s /etc/resolv.conf may point to an unreachable nameserver; fix with docker run --dns 8.8.8.8. Second, minimal base images (Alpine, distroless) ship without a CA bundle; install with apk add ca-certificates. Third, the container’s network mode may not allow outbound traffic to the host; try --network host as a diagnostic, then switch to bridge with proper routing for production.
Should I migrate from curl to HTTPie or wget?
For interactive API debugging, HTTPie or xh have better defaults (color, JSON formatting, auto-redirect). For scripts that need predictable retry behavior, wget’s default --tries=20 is more forgiving than curl’s no-retry default. For HTTP/3 specifically, install a curl with quiche/ngtcp2 or use xh. curl remains the most universal choice for production scripts because it is preinstalled everywhere and has the most predictable flag surface across distros.
Related: For SSL certificate issues in Git, Node.js, and Python, see Fix: SSL certificate problem: unable to get local issuer certificate. If you’re debugging a server that returns 502 behind nginx, see Fix: Nginx 502 Bad Gateway. For CORS errors when making requests from the browser, see Fix: Access to fetch has been blocked by CORS policy.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: SSL certificate problem: unable to get local issuer certificate
How to fix 'SSL certificate problem: unable to get local issuer certificate', 'CERT_HAS_EXPIRED', 'ERR_CERT_AUTHORITY_INVALID', and 'self signed certificate in certificate chain' errors in Git, curl, Node.js, Python, Docker, and more. Covers CA certificates, corporate proxies, Let's Encrypt, certificate chains, and self-signed certs.
Fix: Certbot Certificate Renewal Failed (Let's Encrypt)
How to fix Certbot certificate renewal failures — domain validation errors, port 80 blocked, nginx config issues, permissions, and automating renewals with systemd or cron.
Fix: Valkey Not Working — Redis Client Compatibility, ACL, Cluster Mode, and Migration
How to fix Valkey errors — client connection refused, RESP protocol compatibility, ACL user setup, cluster slot reshard, persistence config (RDB/AOF), TLS, Sentinel mode, and migrating from Redis.
Fix: Docker Container Keeps Restarting
How to fix a Docker container that keeps restarting — reading exit codes, debugging CrashLoopBackOff, fixing entrypoint errors, missing env vars, out-of-memory kills, and restart policy misconfiguration.