Troubleshooting WebSocket Connection Refused Errors
The browser console says WebSocket connection to 'ws://...' failed.
You check your WebSocket code. You read the docs. You add error
handlers. None of that helps, because the problem has nothing to do
with WebSocket.
“Connection refused” is a TCP error. It means no process is accepting connections on the target host and port. The WebSocket handshake never even started. Every minute you spend debugging your WebSocket code is a minute wasted on the wrong layer.
The diagnostic checklist
Section titled “The diagnostic checklist”Work through these in order. Stop at the first failure — everything downstream depends on TCP connectivity.
1. Is the server process running?
Section titled “1. Is the server process running?”This sounds obvious, but it’s the most common cause. The server crashed, didn’t start, or is listening on a different port than you expect.
# Check if anything is listening on your expected portlsof -i :8080 | grep LISTEN# or on Linuxss -tlnp | grep 8080If nothing shows up, your server isn’t running or isn’t bound to that port. Check logs, check your start command, check for port conflicts.
2. Are you connecting to the right host and port?
Section titled “2. Are you connecting to the right host and port?”Mismatches between your client URL and your server’s actual
bind address are surprisingly common. Hardcoded localhost in
development code that gets deployed. Port 8080 in the client,
port 3000 in the server config. ws:// when the server expects
wss://.
// Common mistake: hardcoded dev URL in productionconst ws = new WebSocket("ws://localhost:8080/ws");
// What you probably need in productionconst ws = new WebSocket( `wss://${window.location.host}/ws`);3. Is a firewall blocking the port?
Section titled “3. Is a firewall blocking the port?”Cloud security groups, OS firewalls, and corporate networks all filter traffic. The connection will be refused (or silently dropped) if the port isn’t explicitly allowed.
# Test raw TCP connectivitync -zv your-server.com 8080
# If using AWS, check the security group allows inbound# on your WebSocket port from the client's IP rangeA “connection refused” response means the port is reachable but nothing is listening. A timeout with no response means a firewall is dropping packets silently. Different problems, different fixes.
4. Is your reverse proxy forwarding Upgrade headers?
Section titled “4. Is your reverse proxy forwarding Upgrade headers?”This is the single most common cause of WebSocket failures in production. Nginx, Apache, Caddy, and cloud load balancers all require explicit configuration to forward the HTTP Upgrade handshake that initiates a WebSocket connection.
Without Upgrade headers, the proxy treats the handshake as a regular HTTP request. The server never sees the upgrade, the client gets a non-101 response, and the connection fails.
The Nginx fix (most common solution)
Section titled “The Nginx fix (most common solution)”Nginx does not pass Upgrade headers by default. You must add them explicitly:
location /ws { proxy_pass http://backend:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s;}Every line matters:
proxy_http_version 1.1— WebSocket requires HTTP/1.1 for the Upgrade handshake. Nginx defaults to 1.0 for upstream connections.proxy_set_header Upgrade— forwards the client’s Upgrade header.proxy_set_header Connection "upgrade"— tells the upstream to switch protocols.proxy_read_timeout 3600s— Nginx’s default is 60 seconds. Idle WebSocket connections will be killed after that. Set this to match your application’s expected connection lifetime.
Missing any one of these headers will silently break WebSocket. For a full production config including SSL termination and load balancing, see the Nginx WebSocket configuration guide.
Docker networking gotchas
Section titled “Docker networking gotchas”Docker adds a network namespace boundary that catches people off guard. Two problems come up repeatedly:
Binding to 127.0.0.1 inside the container. When your server
binds to localhost or 127.0.0.1, it only accepts connections
from inside the container. Docker’s port mapping (-p 8080:8080)
routes traffic from the host to the container’s network interface,
but that traffic arrives on 0.0.0.0, not 127.0.0.1.
// WRONG: only reachable inside the containerserver.listen(8080, "127.0.0.1");
// RIGHT: accepts connections from Docker's port mappingserver.listen(8080, "0.0.0.0");Container-to-container communication. If your WebSocket
client runs in one container and the server in another, localhost
refers to the client’s own container. Use Docker’s service
names (in Compose) or the container’s IP on the shared network.
services: app: depends_on: [ws-server] environment: # NOT localhost — use the service name WS_URL: ws://ws-server:8080/ws ws-server: ports: - "8080:8080"Cloud load balancer configuration
Section titled “Cloud load balancer configuration”Cloud load balancers support WebSocket, but their defaults are tuned for short-lived HTTP requests. Without adjustment, they’ll terminate idle WebSocket connections.
AWS Application Load Balancer (ALB)
Section titled “AWS Application Load Balancer (ALB)”ALB handles WebSocket natively — no special listener configuration. But two defaults will bite you:
- Idle timeout defaults to 60 seconds. A WebSocket connection with no traffic for 60 seconds gets terminated. Increase this to 3600 seconds in the ALB attributes, and implement application-level ping/pong to keep connections alive.
- Sticky sessions must be enabled on the target group. WebSocket connections are stateful — if a health check routes a subsequent request to a different backend, the connection breaks.
Cloudflare
Section titled “Cloudflare”Cloudflare proxies WebSocket connections on all plans, but you must confirm it’s enabled in the dashboard under Network > WebSockets. Without this toggle, Cloudflare will not forward Upgrade headers and your handshake fails with a 101 not received error.
Cloudflare also enforces a 100-second idle timeout on free and pro plans. Send periodic ping frames to keep connections alive, or the connection will be silently closed.
HAProxy
Section titled “HAProxy”HAProxy requires tunnel mode for WebSocket connections after
the handshake:
frontend ws_front bind *:443 ssl crt /etc/ssl/cert.pem acl is_websocket hdr(Upgrade) -i WebSocket use_backend ws_back if is_websocket
backend ws_back timeout tunnel 3600s server ws1 10.0.0.1:8080 checkThe timeout tunnel directive controls how long HAProxy keeps
the bidirectional connection open. Without it, HAProxy uses the
standard HTTP timeout, which is far too short for WebSocket.
HTTPS/WSS mismatch
Section titled “HTTPS/WSS mismatch”If your page is served over HTTPS, the browser will refuse to
open a ws:// connection. Mixed content rules apply. You must
use wss:// from HTTPS pages.
The reverse is also a problem: if you connect to wss:// but
your server isn’t configured for TLS, the TLS handshake fails
before the WebSocket handshake even starts. The error often
looks like a connection refused rather than a certificate error.
In production, terminate TLS at the load balancer or reverse
proxy and proxy to the backend over plain ws://. The client
connects with wss://, the proxy handles the certificate, and
the backend doesn’t need to deal with TLS.
Managed WebSocket services handle TLS
termination and proxy configuration for you, which eliminates
this entire category of problem.
How to test and debug
Section titled “How to test and debug”Command-line tools
Section titled “Command-line tools”wscat is the fastest way to test from a terminal:
# Installnpm install -g wscat
# Test a connectionwscat -c ws://localhost:8080/ws
# Test with TLSwscat -c wss://your-server.com/wswebsocat gives you more control — useful for debugging protocol-level issues:
# Test with verbose outputwebsocat -v ws://localhost:8080/ws
# Send a message and disconnectecho "ping" | websocat ws://localhost:8080/wsBrowser DevTools
Section titled “Browser DevTools”Open DevTools, go to the Network tab, and filter by WS. You’ll see every WebSocket connection attempt, the handshake request/response headers, and any frames sent or received.
If the connection fails, the Console tab will show the error. Pay attention to the exact message — “connection refused” is different from “unexpected response code 502” (proxy issue) or “was not upgraded to websocket” (missing Upgrade headers).
Isolating the layer
Section titled “Isolating the layer”If you’re not sure whether the problem is TCP, TLS, HTTP, or WebSocket, work up the stack:
# Layer 1: Can you reach the port? (TCP)nc -zv server.com 8080
# Layer 2: Does TLS work? (if using wss://)openssl s_client -connect server.com:443
# Layer 3: Does the HTTP upgrade work?curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ http://server.com:8080/wsIf curl gets a 101 Switching Protocols response, the server
is fine and the problem is in your client code. If it gets a 400
or 502, the issue is at the proxy or server level.
Frequently Asked Questions
Section titled “Frequently Asked Questions”What does “WebSocket connection to ws:// failed” mean?
Section titled “What does “WebSocket connection to ws:// failed” mean?”The browser tried to open a TCP connection to the host and port
in your WebSocket URL, and the operating system refused it. No
WebSocket protocol exchange happened. The server process is
either not running, not listening on that port, or a firewall is
blocking access. Start debugging at the network layer — use
nc -zv host port to verify raw TCP connectivity before looking
at WebSocket-specific configuration.
Why does my WebSocket work locally but fail behind Nginx?
Section titled “Why does my WebSocket work locally but fail behind Nginx?”Nginx doesn’t forward HTTP Upgrade headers by default. When a
client sends the WebSocket handshake, Nginx proxies it as a
regular HTTP request, stripping the Upgrade and Connection
headers. The backend never receives the upgrade request and
responds with a normal HTTP response, which the client rejects.
Add proxy_set_header Upgrade $http_upgrade and
proxy_set_header Connection "upgrade" to your location block.
See the Nginx guide for a full
production config.
How do I test if a WebSocket server is reachable?
Section titled “How do I test if a WebSocket server is reachable?”Use wscat -c ws://host:port from the command line for a quick
check. If that fails, drop down to TCP: nc -zv host port tells
you whether anything is listening. If TCP works but wscat fails,
the problem is in the HTTP upgrade — use curl with Upgrade
headers to see the server’s response. In the browser, the
Network tab filtered to WS shows handshake details including
response headers and status codes.
Why does my WebSocket fail in Docker but work on the host?
Section titled “Why does my WebSocket fail in Docker but work on the host?”Almost always a bind address issue. Server processes that bind
to 127.0.0.1 inside a container only accept connections from
within that container’s network namespace. Docker’s -p port
mapping routes external traffic to 0.0.0.0, which doesn’t
match. Change your server’s bind address to 0.0.0.0. For
container-to-container communication, use Docker Compose
service names instead of localhost.
How do I fix WebSocket timeouts on cloud load balancers?
Section titled “How do I fix WebSocket timeouts on cloud load balancers?”Every cloud load balancer has an idle timeout — typically 60 seconds by default. If no data crosses the connection within that window, the load balancer terminates it. Set the idle timeout higher (3600 seconds is common), and implement application-level ping/pong frames at an interval shorter than the timeout. On AWS ALB, this is in the load balancer attributes. On Cloudflare, the timeout varies by plan and can’t be changed on free tier — you must keep connections active with pings.
Related Content
Section titled “Related Content”- Nginx WebSocket Configuration — full proxy config with SSL, timeouts, and load balancing
- AWS ALB WebSocket Setup — target groups, sticky sessions, and idle timeout tuning
- Cloudflare WebSocket Config — enabling WebSocket proxying and working within plan limits
- WebSocket Reconnection Patterns — exponential backoff, state recovery, and jitter strategies
- WebSocket Close Codes Reference — understand what the server is telling you when connections drop