Skip to content

Connection refused, timed out, and the rest

Six error strings account for nearly every failed connection on a Linux system. Each one is generated at a different point in the journey, by a different party, and each one eliminates a different set of possible causes. Learning to read them is the highest-value hour in network troubleshooting — and "what's the difference between connection refused and connection timed out?" is asked in interviews precisely because the answer reveals whether someone understands what a packet actually does.

The table to memorise

Error Kernel errno Who produced it What it proves
Name or service not known Your own resolver No packet was ever sent. This is a DNS problem, not a network one
Network is unreachable ENETUNREACH Your own kernel No packet was sent. Your routing table has no route for that destination
No route to host EHOSTUNREACH Usually a router on the path The packet travelled, and something along the way reported it couldn't deliver it
Connection refused ECONNREFUSED The destination host The packet arrived and a live host answered — with a TCP reset, because nothing is listening on that port
Connection timed out ETIMEDOUT Your own kernel, after giving up The packet went out and nothing came back. Something dropped it silently
Connection reset by peer ECONNRESET The other end, mid-conversation The connection was established and then killed

The whole table pivots on one question: did anything come back? A refusal means yes, something answered. A timeout means no, and silence is a deliberate choice somebody's firewall made.

Refused: the host is alive, the port isn't

nc -zv -w 3 10.20.0.31 5432
nc: connect to 10.20.0.31 port 5432 (tcp) failed: Connection refused

On the wire, this is one packet out and one packet back:

10:31:07.412885 IP 10.20.0.12.51402 > 10.20.0.31.5432: Flags [S], seq 2847193021, win 64240, length 0
10:31:07.413094 IP 10.20.0.31.5432 > 10.20.0.12.51402: Flags [R.], seq 0, ack 2847193022, win 0, length 0

The [R.] is a TCP reset, sent by the destination's own kernel. It's the standard, correct response when a SYN arrives for a port with no listening socket.

What this rules out is substantial: DNS resolved, routing works in both directions, no firewall dropped anything, and the host is powered on and running a TCP/IP stack. The failure is entirely local to that machine — either the service isn't running, or it is running but bound to the wrong address.

That second case is the one that wastes afternoons. Check it on the server itself:

sudo ss -tlpn sport = :5432
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      244        127.0.0.1:5432      0.0.0.0:*   users:(("postgres",pid=1204,fd=7))

There's the whole bug. Postgres is running and healthy; it's listening on loopback, so it can only ever be reached from the machine itself. Nothing about the network needs changing — listen_addresses in the service's own configuration does.

Note the speed: the refusal came back in 0.2 milliseconds. A connection that fails instantly is almost always refused; one that fails slowly is almost always dropped. You can often tell which of the two you're dealing with before reading the message.

Timed out: something is dropping packets and not admitting it

nc -zv -w 5 10.20.0.31 9200
nc: connect to 10.20.0.31 port 9200 (tcp) failed: Connection timed out

On the wire, only your side speaks — the same SYN, retried on an exponential backoff, with no reply of any kind:

10:33:02.104881 IP 10.20.0.12.51410 > 10.20.0.31.9200: Flags [S], seq 918273645, win 64240, length 0
10:33:03.121004 IP 10.20.0.12.51410 > 10.20.0.31.9200: Flags [S], seq 918273645, win 64240, length 0
10:33:05.153221 IP 10.20.0.12.51410 > 10.20.0.31.9200: Flags [S], seq 918273645, win 64240, length 0

Same sequence number every time — this is one connection attempt being retransmitted, not three attempts.

Silence is a policy. Firewalls offer two ways to block traffic, and the choice between them is exactly what you're observing:

  • DROP (netfilter) / deny (a cloud security group) — discard the packet, send nothing. The client waits out its full retry sequence and reports a timeout. This is the common default, chosen because it gives a scanner no information.
  • REJECT — discard the packet and send back an ICMP administratively-prohibited message or a TCP reset. The client fails immediately with Connection refused or No route to host.

Which means a timeout narrows things to a short list: a firewall rule with DROP, a cloud security group or network ACL that doesn't allow the port, a machine that's powered off, or an address that doesn't exist. It is not usually the application, because the application never saw anything.

Note also how long the wait is when nothing bounds it. Linux retries a SYN several times with doubling delays, which adds up to well over two minutes before ETIMEDOUT. That's why -w 3 belongs in every reachability test you type, and why an application without a connect timeout configured can hold a worker thread hostage for minutes on a single dead backend.

The one-line difference worth being able to say out loud

Refused: your packet arrived and was actively rejected. Timed out: your packet vanished and nobody replied. The first is a service or bind-address problem on a reachable host; the second is a filtering or reachability problem in between.

The two "unreachable" errors, which are not the same

nc: connect to 203.0.113.9 port 443 (tcp) failed: Network is unreachable

This one never left your machine. The kernel consulted its routing table, found no entry matching that destination — not even a default route — and gave up before generating a packet. It's a local configuration problem, and one command confirms it:

ip route get 203.0.113.9
RTNETLINK answers: Network is unreachable

The usual causes are a missing default route (common in containers with custom networking, and on hosts where a DHCP lease expired) and an IPv6 address on a host with no IPv6 route.

nc: connect to 10.20.0.99 port 443 (tcp) failed: No route to host

This one is different despite the similar wording. A packet did leave, and something reported back that it couldn't be delivered — typically an ICMP Destination Unreachable message from a router on the path, or, on your own LAN, your host's own failure to resolve the target's MAC address because nothing at that address answered ARP. On a local subnet, that means the IP is unused or the machine is off.

Distinguishing them: Network is unreachable is answered by ip route, No route to host is answered by ip neigh and by asking whether that host exists at all.

Reset by peer: it worked, and then it didn't

curl: (56) Recv failure: Connection reset by peer

This is a different category from all of the above, because the connection succeeded. Data was flowing, and then one side sent an RST. Something killed a live conversation.

The realistic causes, roughly in order of how often they turn out to be the answer:

  • The server-side process crashed or was restarted mid-request — check its own logs at that timestamp first.
  • A load balancer or proxy hit an idle timeout and closed a connection the client thought was still usable. Classic in connection pools: the pool hands out a socket the far end closed thirty seconds ago.
  • A stateful firewall dropped its connection-tracking entry for an idle connection, then reset the next packet because it no longer recognised it. TCP keepalives on a shorter interval than the firewall's idle timeout are the standard fix.
  • The application deliberately rejected the request after reading part of it — a TLS handshake failure or a request body exceeding a size limit both look like this from the client's side.

A capture settles which end sent the reset, and that alone splits the search in half:

sudo tcpdump -i any -nn 'host 10.20.0.44 and tcp[tcpflags] & tcp-rst != 0'

Read the source address of the RST packet. If it came from the server's address, the server (or something impersonating it, like a transparent proxy) ended the connection. If it came from your own address, your side did — and the reason is in your application, not theirs.

Putting it together: one symptom, four verdicts

An internal service at 10.20.0.31:9200 is unreachable from app-02. Same command, four possible outcomes, four different next steps:

nc -zv -w 3 10.20.0.31 9200
Result Verdict Next command
succeeded! Network is fine; the problem is above TCP curl -v against the service
Connection refused Host is up, service isn't listening there sudo ss -tlpn on 10.20.0.31
Connection timed out Something is silently dropping the packet sudo nft list ruleset on both ends, and the cloud security group
No route to host The address isn't answering on the local segment ip neigh show 10.20.0.31, and confirm the host is running

Four seconds of work, and each branch has eliminated three of the four possible worlds. That's what the error strings are for.

Practice

  1. On your own machine, produce each of the first five errors in the table deliberately: pick a closed port for refused, a firewalled port for timed out, an unrouted address for network unreachable, a nonexistent LAN address for no route to host, and a typo'd hostname for the DNS error. Note the elapsed time for each.
  2. Add a DROP rule for a port on a lab VM, test it, change the rule to REJECT, and test again. Explain, from the client's error message alone, which rule was active.
  3. Capture the traffic during a Connection refused and during a Connection timed out. Count the packets in each capture and explain the difference in one sentence.
  4. Find a connection pool or HTTP client in a codebase you work on and check whether it sets a connect timeout. If it doesn't, work out how long a request would block against a silently-dropping destination, using the retry behaviour described above.

Exercise 4 is not a networking exercise disguised as a coding one — it's the reason this distinction matters in production. A service that treats "refused" and "timed out" identically will retry a dead backend for minutes and exhaust its own worker pool, turning one unreachable dependency into a full outage. The two failures deserve different timeouts and different retry policies, and you can only write those if you know which one you're getting.

Sources