Connection refused and Connection timed out look like siblings — SSH didn’t connect either way — but they are opposite findings, and they point at different halves of the stack. Knowing which one you’re looking at cuts the suspect list in half before you type a single diagnostic command. This is the two-minute routine: what each error proves, the one probe that separates them from everything else, and a checklist for each branch. (For the errors that happen after the TCP connection succeeds — auth failures, host key warnings, dying sessions — see the decoder ring.)
What each error actually proves
Connection refused means the machine answered you. Your TCP SYN reached a live host, and the host actively said no — a RST packet or an ICMP port-unreachable came back. That’s a surprising amount of good news: the IP is right, the route works, the host is up, and nothing on the path is eating your packets. The problem is narrow: nothing is listening on that port, or a firewall rule is rejecting (not dropping) it.
Connection timed out means silence. Your packets left and nothing ever came back. That proves almost nothing about the server — it may be down, the IP may be wrong, or (most often) something between you and it is silently dropping packets: a cloud security group, a firewall with a DROP policy, a NAT or VPN black hole.
One mnemonic: refused = the machine spoke to you; timed out = it never did. Refused is a host problem. Timed out is usually a path problem.
Step 1: probe the port, not the host
Skip ping as your first move — it tests the wrong thing in both directions. A host that answers ping can still drop port 22 (cloud firewalls filter per port), and a host that ignores ping can be serving SSH happily (ICMP is blocked all over the place). Ping proves nothing you need.
Probe the actual port:
nc -vz example.com 22
(No netcat? telnet example.com 22 works — any output starting SSH-2.0-... means sshd answered.) Three outcomes:
Connection refused— take the refused branch below.- Hangs, then times out — take the timed-out branch.
- Succeeds — TCP is fine and your problem is inside SSH itself: wrong user or key (
Permission denied (publickey)has its own guide), a host key mismatch, or somethingssh -vwill name outright.
The refused branch: find what’s (not) listening
You’ll need another way onto the box — the provider’s web console exists for exactly this moment.
Is sshd running?
systemctl status sshd # the unit is "ssh" on Debian/Ubuntu
If it’s dead, don’t just restart it — check why: journalctl -u sshd (or -u ssh) shows how it exited. A frequent cause is a config edit that doesn’t parse; sshd -t validates the file and names the bad line before you restart into the same failure.
Is it listening where you think?
sudo ss -tlnp | grep sshd
Two gotchas hide in this output. The port: someone moved SSH to 2222 and you’re knocking on 22 — connect with ssh -p 2222, then pin it per host in ~/.ssh/config so you never re-derive it. The address: a ListenAddress line can bind sshd to a single interface, so it answers on the private IP and refuses on the public one.
Is the local firewall rejecting? sudo ufw status, sudo iptables -L -n, or nft list ruleset — look for REJECT rules covering your port. A REJECT rule produces exactly this error: the host is up, sshd may even be listening, and the firewall answers on its behalf.
Have you been banned? fail2ban’s default action rejects, so a ban looks like Connection refused — from your IP only. The tell: it works from a phone hotspot but not from your network. From the console: fail2ban-client status sshd, then fail2ban-client unban <your-ip> — and fix whatever retried a bad credential often enough to earn the ban.
The timed-out branch: find where packets die
Confirm the address first. It’s embarrassing exactly once. ssh -v prints the IP it actually dials — check it against the provider’s dashboard. Stale DNS after a migration, and the elastic IP that didn’t survive an instance stop, are both classics.
Check the cloud firewall next, because it’s the answer most of the time. Security groups (AWS), NSGs (Azure) and VPC firewall rules (GCP) default to dropping inbound traffic — silently, which is precisely a timeout. Look for an inbound rule allowing TCP 22 from your address. The sneaky variant: a rule locked to “My IP” months ago, and your IP has changed since. Network ACLs at the subnet level are a second, less-visited layer worth a look when the security group seems right.
Trace the path when the cloud rules check out:
traceroute example.com # mtr example.com for a live view
sudo traceroute -T -p 22 example.com # TCP to the actual port — closest to the truth
Plain traceroute uses UDP or ICMP, which firewalls treat differently from TCP 22 — the -T -p 22 form tests what SSH actually experiences. Where the hops stop answering is roughly where your packets die.
Suspect your own side. Hotel, café and office guest networks routinely block outbound 22, and corporate VPNs reroute or drop it. The isolation test is the phone hotspot: if SSH works there, your server is fine and the network you were on is the problem.
A DROP-policy firewall on the host itself looks identical to a cloud-side drop from the outside — if you have console access, check iptables/nftables on the box too.
The exotic one: port knocking. A server running knockd drops everything until it sees a secret sequence of connection attempts on other ports. If a predecessor set up the box and every port times out from everywhere, ask around before you burn an afternoon on routing theories.
Keep the two-minute version
The whole routine compresses well: probe the port with nc -vz. Refused → console in, check sshd, the listening port, REJECT rules, fail2ban. Timed out → confirm the IP, check the cloud security group, then traceroute and your own network. Ping appears nowhere in it.
The recurring facts — which port, which user, which host needs keepalives — belong in ~/.ssh/config, not in your memory. And part of the diagnosis can be standing state instead of a mid-incident scramble: Termalin stores the port, user, key and keepalive settings with each saved host and shows live status per host — so “is the box even up?” and “which port was it again?” are answered before you connect, and the server that only listens on 2222 stops being a thing you rediscover quarterly.
Termalin is a free, cross-platform SSH client that keeps per-host ports, users, keys and keepalives with the host and shows live host status — download it, or browse the full feature list.