A "connection refused" error means the source successfully resolved DNS, opened a TCP connection to your IP, and got an explicit RST or "no listener" response. The request never reached your application — your server is up enough to refuse, but no process is accepting connections on the expected port.
Root Causes
1. Application crashed or stopped
Your Node / Python / Go process exited and didn't restart. Reverse proxy responses (nginx, Caddy) would return 502 in this case — but if there's no reverse proxy, just direct TCP to your app, the OS itself returns RST.
2. Application listening on the wrong interface
Your app binds to 127.0.0.1:3000 instead of 0.0.0.0:3000. Localhost-only — external connections refused. Common in Express apps where you use app.listen(3000, '127.0.0.1') or accept the framework default that binds to localhost.
3. Firewall / security group blocking
AWS Security Group, GCP firewall, ufw, iptables — any of these can drop incoming traffic on the webhook port. The TCP behavior is RST or DROP depending on config; both look like "connection refused" to the client.
4. Port mismatch
You registered the webhook URL with port 8080 but your app moved to port 3000 during a refactor. Or your reverse proxy is configured to listen on the wrong port. Check the URL in the provider's dashboard against what's actually listening.
5. Hostname resolves to old IP
Your A record points to an old IP that's been reassigned to a different VM. The new IP refuses on whatever port you registered.
Diagnostic Sequence
# 1. From your own server, confirm the app is listening
ss -tlnp | grep -E ':(80|443|3000|8080)'
# Should show: LISTEN 0.0.0.0:3000 (or :::3000 for IPv6)
# If it shows 127.0.0.1:3000 — bind interface is wrong
# 2. From outside the server, confirm the port is reachable
curl -v https://your-domain.com/webhook
# "Connection refused" → port is closed externally
# 3. Check firewall rules
sudo ufw status
sudo iptables -L INPUT -n
# AWS:
aws ec2 describe-security-groups --group-ids sg-xxx
# 4. Confirm DNS resolves to the right IP
dig +short your-domain.com
# Compare against your actual server IP
Fix It
Bind to all interfaces
// Express — accept connections from any IP
app.listen(3000, '0.0.0.0', () => {
console.log('listening on 0.0.0.0:3000')
})
Open the firewall port
# ufw
sudo ufw allow 443/tcp
# AWS Security Group
aws ec2 authorize-security-group-ingress \
--group-id sg-xxx --protocol tcp --port 443 --cidr 0.0.0.0/0
Process supervisor
Run your app under systemd, Docker (with restart policy), or PM2 so it auto-recovers from crashes.
# Docker Compose
services:
webhook-handler:
image: your-app:latest
restart: unless-stopped
ports:
- "443:3000"
How to Reproduce
Stop your application: docker stop your-handler or kill. Fire a test webhook from WebhookWhisper or curl your endpoint — connection refused. Restart the app, retry — connection succeeds. The diagnostic sequence above maps each failure mode to a specific cause.
Frequently Asked Questions
What's the difference between connection refused and connection reset?
Refused = the OS rejected the TCP SYN immediately (no listener, firewall DROP/RST). Reset = the connection was established and then dropped mid-way (process crashed, idle timeout). Both signal different operational issues.
Why does my app work locally but show connection refused in production?
Almost always one of: (a) bound to 127.0.0.1 in production, (b) prod firewall blocks the port, (c) prod port differs from local, (d) prod is behind a reverse proxy that's not running.
How do I prevent connection-refused errors during deploys?
Use a process manager that does graceful shutdown + zero-downtime restarts. Docker rolling updates, systemd with socket activation, or PM2 cluster mode all handle this.