Problem
curl: (7) Failed to connect to example.com port 443 after 2 ms: Connection refused. It looks like a network failure, and half the advice online treats it as one — flush DNS, restart the machine, check your internet. But exit code 7 is more specific than “the network is down,” and reading it literally saves you an hour of guessing. It tells you exactly how far curl got before it gave up, and that spot on the map is the whole diagnosis.
The one thing exit 7 rules out immediately: this is not a DNS problem. That’s a different code entirely — (6) Could not resolve host. If you got 7, the name already resolved to an IP. curl knew where to go. It just couldn’t get a TCP connection to open once it got there.
What exit 7 means, precisely
CURLE_COULDNT_CONNECT (7) means the TCP three-way handshake to the target host and port failed. Nothing above the transport layer ran — no TLS negotiation, no HTTP request, no certificate check. curl sent a SYN and the connection never came up. There are only three reasons that happens, and the trailing text of the error tells you which one you’re looking at.
Refused vs timed out: read the words after the code
This is the fork the whole diagnosis hangs on, and curl hands it to you for free:
“Connection refused” — the host is up and reachable, but it actively said no. Its kernel replied with a TCP RST because nothing is listening on that port. This is a fast failure; it comes back in milliseconds, because a rejection is a real, prompt answer. It means one of:
- the service you’re trying to reach isn’t running,
- it’s running but bound to a different port (or to
127.0.0.1only, so it refuses external IPs), - you’ve got the wrong port number.
“Connection timed out” (or “No route to host”) — your SYN went out and nothing came back. No rejection, no reply, just silence until curl gives up. This is a slow failure; it hangs for seconds. Silence at the transport layer is the signature of a firewall or cloud security group dropping the packet rather than rejecting it. A dropped packet is deliberately indistinguishable from an unplugged cable — that’s what packet-filtering firewalls are designed to look like. It means:
- a firewall rule (host, network, or cloud security group) is blocking the port,
- the host is genuinely unreachable (down, or no route).
The response time alone tells you which before you read a single word: refused is instant, timed out hangs. A closed door versus a wall you can’t see.
Note on exit codes: a connection that times out normally still returns exit 7 with “Connection timed out” text. If you see exit 28 (
CURLE_OPERATION_TIMEDOUT) instead, that’s curl’s own timeout budget expiring — you hit--connect-timeout,--max-time, or the default operation limit — which is a slightly different statement: “I gave up waiting,” not “the OS returned a connect error.” For diagnosis, treat 28 like the timed-out branch of 7.
The proxy trap
Before you touch firewalls, rule out the quiet one. curl obeys the http_proxy, https_proxy, and ALL_PROXY environment variables automatically — no --proxy flag required. A stale proxy left in a shell profile, a container image, or a CI runner makes curl try to connect to the proxy, and it fails with exit 7 against the proxy’s address. The giveaway is in the message: it says Failed to connect to some-proxy.internal port 3128, not to your actual target.
env | grep -i proxy # see what's set
curl --noproxy '*' https://example.com # bypass it entirely
If --noproxy '*' makes the request succeed, the target was fine all along and your environment was pointing curl at a dead proxy. This is the reason “works in the browser, fails in the terminal” is almost always a proxy config, not a server problem — the browser and the shell don’t share the same proxy settings.
How to diagnose it in order
- Run
curl -vfirst. Verbose mode prints each stage —Trying <IP>..., then the connect result — so you see exactly where it died and against which IP and port. - Read the failure word. Refused → jump to step 4. Timed out → jump to step 5.
- Check for a proxy.
env | grep -i proxy; retry with--noproxy '*'. If the message names a proxy host, that’s your answer. - If refused: confirm the service is actually running and bound to that port on that interface (
ss -tlnpon the host). Check you have the right port. A service bound to127.0.0.1refuses every non-local client. - If timed out: check the firewall / cloud security group for that port. Test the port from outside the host — from your own network, the port is either reachable or filtered, and that tells you whether the block is at the host or in front of it.
Check it with DechoNet
- Port Check tests the port from outside, the way curl’s target sees the internet. It distinguishes open (something’s listening and reachable), closed/refused (reachable host, nothing on the port), and filtered (silent — a firewall is dropping packets) — which is the exact refused-vs-timed-out fork above, answered from a vantage point that isn’t your own possibly-misconfigured machine.
- DNS Lookup confirms the hostname resolves to the IP you expect — worth a glance to be sure curl isn’t connecting to a stale or wrong address that merely happens to resolve.
Resolution Checklist
- Confirm it’s exit 7, not 6 — 7 means DNS already worked; the connection failed.
- Read the text after the code: “refused” (fast, port closed) vs “timed out” (slow, firewall drop).
- Rule out a proxy:
env | grep -i proxy, then retry withcurl --noproxy '*'. - Refused → verify the service is up and bound to the right port and interface.
- Timed out → check the firewall / security group, and test the port from outside the host.
When to Escalate
- If the port checks open from outside but curl still fails locally, the block is on your side of the path — a local firewall, VPN split-tunnel, or
/etc/hostsoverride sending you to the wrong IP. - If it’s refused only intermittently, the service is likely crashing and restarting, or a load balancer is routing some connections to a dead backend — that’s an availability problem behind the port, not a connectivity one, and belongs with whoever owns the service.
Check your own domain now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.