What the error actually says
Chrome shows “This site can’t be reached. example.com refused to connect.” with ERR_CONNECTION_REFUSED (net error -102). Firefox says “Unable to connect”. Programs get ECONNREFUSED, “Connection refused”.
Every TCP connection starts with your machine sending a SYN packet: “I’d like to talk on port 443.” A refusal means a machine at that address received the SYN and answered with a RST: “nobody here takes calls on that port.” The connection never opened.
That makes refused, oddly, the most informative of the connection errors. You know the IP address is right enough to reach something, and you know that something deliberately said no. Compare it with its siblings:
| Error | What happened | How it feels | Where to look |
|---|---|---|---|
ERR_CONNECTION_REFUSED | The SYN was answered with a RST | Fails immediately | No program listening on that port, or a firewall that rejects |
ERR_CONNECTION_TIMED_OUT | The SYN got no answer | Hangs, then fails | Firewall that drops, wrong IP, routing |
ERR_CONNECTION_RESET | Connection opened, then a RST arrived | Fails after connecting | Application, proxy, filter that inspects traffic |
The RST usually comes from the target’s own kernel, which does this automatically when no socket is listening on the port. Less often it comes from a firewall configured to reject rather than silently drop.
If your error is ERR_NAME_NOT_RESOLVED instead, you’re one step earlier: the browser never found an IP address to connect to. That’s a DNS problem, covered in ERR_NAME_NOT_RESOLVED.
Is it localhost, or a real site?
Most people who see this error are looking at localhost refused to connect, on their own development machine. That case has its own short list, so it comes first.
localhost refused to connect
The server isn’t running, or crashed on startup. Look at the terminal where you started it. A port conflict (EADDRINUSE) or a missing dependency kills it before it ever listens.
Wrong port. The dev server printed the port it chose. Vite uses 5173, Create React App 3000, Django 8000, Flask 5000. If the port you wanted was taken, some tools pick the next free one and say so in the output, which is easy to miss.
IPv6 versus IPv4. localhost can resolve to ::1 (IPv6) as well as 127.0.0.1. A server listening only on 127.0.0.1 refuses a connection to ::1. This is behind the familiar Node.js message connect ECONNREFUSED ::1:3000: since Node 17, the resolver returns addresses in the order the OS gives them, often ::1 first. Browsers usually try the other address, but scripts and tools may not. Use 127.0.0.1 explicitly, or make the server listen on both.
It’s in a container or VM. A server running inside Docker, WSL or a VM is on a different network stack from your browser. In Docker, the port must be published (-p 3000:3000), and the app inside must listen on 0.0.0.0, not 127.0.0.1. Many dev servers listen only on localhost by default and need a flag to listen on all interfaces: vite --host, flask run --host=0.0.0.0, python manage.py runserver 0.0.0.0:8000, rails server -b 0.0.0.0.
To see what’s actually listening, use ss -ltnp on Linux, lsof -iTCP -sTCP:LISTEN -n -P on macOS, or netstat -ano | findstr LISTENING on Windows.
If you’re visiting someone else’s site
The site may simply not be serving on the port you asked for. Some sites don’t answer on port 80 (plain http://) at all, only on 443. Type https:// explicitly. Recent versions of Chrome try HTTPS first for addresses typed without a scheme, but a bookmarked http:// link still goes to port 80. If the URL has an explicit port (example.com:8080), make sure that’s correct.
Your hosts file. Ad-blocking hosts lists and old development entries map names to 127.0.0.1 or 0.0.0.0. The browser then connects to your own machine, finds nothing listening, and reports a refusal for a site that’s perfectly fine. Check /etc/hosts on Linux and macOS, or C:\Windows\System32\drivers\etc\hosts on Windows, for the site’s name.
Your IP is banned. Tools like fail2ban add firewall rules for addresses that misbehave, for example after repeated failed logins. With a reject rule, the banned address gets refused while everyone else gets through. A shared office or mobile carrier IP can inherit someone else’s ban. If the site works on another network and in the HTTP check, contact the site owner with your IP address (the IP check shows it).
If none of these apply and the site refuses from everywhere, it’s down on the server side. Only the owner can fix that.
If you run the server
Test from a machine outside your own network and read curl’s verbose output:
curl -v https://www.example.com/ -o /dev/null
* Trying 203.0.113.10:443...
* connect to 203.0.113.10 port 443 from 192.0.2.5 port 51234 failed: Connection refused
* Failed to connect to www.example.com port 443 after 31 ms: Couldn't connect to server
curl: (7) Failed to connect to www.example.com port 443 after 31 ms: Couldn't connect to server
The wording varies a little between curl versions; look for “Connection refused” and the IP it tried. Check that IP is the server you think it is with a DNS lookup. Then work through these on the server.
1. Nothing is listening on that port
sudo ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=812,fd=6))
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=1290,fd=21))
In this example nothing listens on 443. A web server configured for HTTP only, where the HTTPS server block was never added or the certificate step failed, looks exactly like this, and HTTPS visitors get refused.
If the service should be running but isn’t, ask why:
systemctl status nginx
journalctl -u nginx -n 50 --no-pager
sudo nginx -t
A web server that refuses to start after a config change, or after a certificate file it references disappeared, is a common cause. nginx -t names the bad line.
2. It listens on 127.0.0.1 only
In the output above, the Node app is on 127.0.0.1:3000. curl localhost:3000 on the server works; every other machine is refused. That’s correct if nginx proxies to it locally, and wrong if you expected to reach port 3000 from outside.
Databases default to this on purpose. PostgreSQL’s listen_addresses defaults to localhost, and the MySQL packages on Debian and Ubuntu set bind-address = 127.0.0.1. If you really need remote access, change the bind address, and then restrict who can connect with a firewall. Most of the time the better answer is an SSH tunnel, and leaving it on loopback.
Check IPv6 too. nginx with listen 80; but no listen [::]:80; refuses IPv6 clients while IPv4 works. Browsers usually fall back to IPv4 and hide it, so test each explicitly:
curl -4 -sI https://www.example.com/ | head -1
curl -6 -sI https://www.example.com/ | head -1
If you don’t serve IPv6, don’t publish an AAAA record.
3. A firewall rejects instead of dropping
A firewall rule can either drop a packet silently (the client times out) or reject it (the client finds out immediately). Rejects show up as refusals, or as “No route to host” when the rule answers with an ICMP “administratively prohibited” message, which firewalld’s default zones do.
sudo iptables -S | grep -i reject
sudo nft list ruleset | grep -i reject
sudo firewall-cmd --list-all
sudo fail2ban-client status
If the refusal only hits certain clients, a fail2ban jail or a rate limiter is the first suspect. fail2ban-client status <jail> lists the banned IPs.
4. DNS points at a server that no longer runs the site
After a migration, the A record (or a stale cached copy) still points at the old server. It’s still up, so it answers, but nothing listens on 443 there anymore. Compare the IP in curl’s output with the server you configured. A propagation check shows whether some public resolvers still hand out the old address.
If you get a 502 page instead
A refusal between your reverse proxy and the application doesn’t reach the browser as ERR_CONNECTION_REFUSED. The proxy accepted the browser’s connection fine; its own connection to the backend failed, so it returns 502 Bad Gateway. nginx logs it as connect() failed (111: Connection refused) while connecting to upstream. The fix is the same list above, applied to the backend port. Behind Cloudflare, the same situation between Cloudflare and your origin shows as Cloudflare error 521.
Checking from outside with DechoNet
The port check opens a TCP connection to the host from DechoNet’s servers and reports each port as Open or Closed. Enter host:port to test one specific port. Read the results with three things in mind:
- Closed doesn’t distinguish refused from timed out. The tool reports a refusal, a reset, and no answer within 3 seconds all as Closed. From outside, a closed port and a firewalled port can’t always be told apart. If you need the difference, run
curl -vand see whether it fails instantly (refused) or hangs (timed out). - It tests from outside your network. Open here and refused for you means the problem is local to you: hosts file, ban, security software.
- Hosts on Cloudflare IPs can’t be tested. Our checks run on Cloudflare’s network, which can’t open TCP connections to Cloudflare’s own address ranges, so the tool reports the host as not checkable instead of showing every port as closed. For a proxied site, test the origin IP directly.
For an HTTPS site specifically, the port 443 checklist goes through the web-server side in more detail.
Related errors
ERR_CONNECTION_TIMED_OUT: no answer at all, rather than a refusal.ERR_CONNECTION_RESET: the connection opened, then got torn down.ERR_NAME_NOT_RESOLVED: DNS didn’t return an address, so no connection was attempted.
Check your own domain now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.