Why WebSocket Will Not Connect: Five Causes, In Order

2026-11-056 min readNetwork Tools4 tools covered

A WebSocket failure is rarely a code bug. Most of them stall during the handshake, which the browser and every device in between get a say in. These are the five causes, ordered by how often they appear.

Key takeaways
  • An https page connecting to ws:// is blocked by mixed content rules before any request leaves the browser, so the address has to be wss://
  • Most frameworks accept only same-origin or whitelisted Origins, and a browser handshake carries the page's own domain, which is an easy 403
  • Nginx has to forward the Upgrade and Connection headers explicitly, otherwise the handshake never reaches the backend
  • A connection stuck with neither open nor error usually means something in the path silently dropped it, which is a network problem rather than a code problem
  • The browser exposes very few request headers, so endpoints that need a token or Cookie cannot be tested from a web tool at all

#Work out which stage fails

A WebSocket failure happens either during the handshake or immediately after it succeeds. The causes do not overlap, so mixing them up sends you editing the wrong layer.

The test is simple: did the log ever say connected? If not, the handshake failed. If it did and then closed, you connected and something else broke.

StageWhat you seeWhere to look
Handshake failsNo connected state, only an errorProtocol, Origin, path, proxy, certificate
Connects then dropsConnected, then closeServer logic, heartbeat, auth timeout
Stuck connectingNeither open nor errorPath silently dropping the request

With the WebSocket tester, a handshake that has not completed after ten seconds triggers a log hint - that hint exists precisely for the stuck case.

#Mixed content comes first

An https:// page connecting to ws:// is blocked by the browser, so no request is ever sent. That is the mixed content policy, and the complaint usually shows in the console rather than the network panel.

The rule is short:

  • Page is https:// - the connection must be wss://
  • Page is http:// - both ws:// and wss:// work

That explains the classic "works locally, fails in production": locally you served over http and used ws://. Switching to wss on production then raises a different question - whether the server has TLS at all.

Warning

Mixed content is blocked inside the browser, so the server log shows nothing at all. If the server sees no connection attempt whatsoever, check the page protocol before touching the server.

#Origin checks: the server refuses

Most WebSocket frameworks accept only same-origin or whitelisted Origins, and a browser handshake carries the domain of the page that opened it. Testing a production endpoint from an online tool means the Origin is the tool's domain, so a rejection is expected behaviour.

The signature is a 403 on the handshake with a matching entry in the server log - the exact opposite of "the server never heard anything".

Which fix applies depends on your side:

  • Server side: add the testing Origin to the whitelist, or drop the Origin check for a public endpoint
  • Client side: switch to a client that can set Origin, which means code rather than a web page
Note

An Origin check is not authentication. Non-browser clients can set it freely; it protects against other websites borrowing a visitor's browser, not against a determined caller connecting directly.

#Path and proxy: the handshake never arrives

Two infrastructure problems account for most of this category: a wrong path, and a reverse proxy that does not forward the upgrade headers. Both look like "cannot connect" and both have different fixes.

Path: most libraries mount at /ws or /websocket, so wss://host alone usually returns 404. Try the full path once.

Proxy, the subtler one: Nginx must forward Upgrade and Connection, otherwise the request is handled as ordinary HTTP and never upgrades.

NGINX
# The Upgrade / Connection lines are what matter
location /ws {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

proxy_http_version 1.1 is required too, because HTTP/1.0 has no protocol upgrade. CDNs such as Cloudflare need WebSocket support enabled separately.

#Untrusted certificate: self-signed fails outright

wss:// performs a normal TLS handshake, and a self-signed certificate fails it - the browser offers no "continue anyway" option. That differs from visiting a self-signed https site, where at least the advanced panel exists.

To confirm: open the https:// form of the domain in the browser and check whether the certificate is trusted. If the browser warns, the wss connection will fail too.

On an internal network with self-signed certificates, either make the client trust the certificate by importing the root, or debug locally with an http page and ws://.

#Check the domain and the port while you are here

A failed handshake can be something far more basic: the name resolves wrong, or nothing is listening on that port. A few minutes with tools beats reading code.

Work through these in order:

  • DNS lookup to confirm the domain resolves to the IP you expect - especially right after a change, when TTL caching gives different networks different answers
  • Port reference to check whether the port is a standard service port and what the firewall should allow. wss uses 443 and ws uses 80, but custom ports such as 8080 and 3000 are common
  • IP address lookup to see whether the resolved address is public or private. Connecting to a 10.x, 172.16-31.x or 192.168.x address from the public internet will never work
Tip

The browser talks to the target server directly, so no message passes through any intermediary. That is good for privacy but means the target sees your real IP - know what an address is before connecting to it.

#Which tool to use

What you want to confirmToolKey point
Whether the handshake succeedsWebSocket testerText frames only, no custom headers
Which IP a name resolves toDNS lookupTTL sets how long the cache lasts
Public or private addressIP address lookupPrivate addresses are unreachable publicly
Whether a port should be openPort referenceThree ranges with different permissions
What the server actually returnedHTTP status codes403 and 404 point at different causes

Order: confirm the protocol (wss?) first, then path and proxy, then Origin, then certificate and network. The first two need no server changes, which makes them the cheapest to rule out.

#Frequently asked questions

#It works locally but not after deployment. Why?

Mixed content is first: the production page is https while the code still opens ws://. Second is the proxy not forwarding the Upgrade headers - locally you connect straight to the backend with no proxy in between. Check in that order and the first one usually lands.

#Is a 403 on the handshake a permissions problem?

In WebSocket context a 403 is far more often a rejected Origin than failed authentication. Look for an Origin or handshake rejection in the server log; if there is no log entry at all, the request never reached the application and the problem is the proxy.

#The connection just sits at "connecting" forever.

That means the handshake neither completed nor failed, so something in the path silently dropped it. Enterprise firewalls, some proxies and CDNs without WebSocket support all behave this way. Identify those devices first, then try from a different network.

#My endpoint needs a token. Can I test it with a web tool?

No. The browser WebSocket API cannot set custom request headers, so Authorization cannot be added. Either use Cookie-based auth on a same-origin page, or test with Postman or a client in code.

#Why is the received message garbled?

The tester handles text frames only and does not parse binary frames. If the server sends binary payloads - image chunks, protobuf, compressed JSON - you need to decode them per the protocol. Garbled Chinese usually means the server is not sending UTF-8.

Tools used in this guide

Related guides