---
title: Why won't my WebSocket connect? How to test it with the WebSocket Tester
description: Your WebSocket works on localhost and fails in production, and the browser console only says the connection failed. To test a WebSocket, open the connection from outside your network and look at the HTTP status of the upgrade: 101 means it works, anything else names the layer that refused it. Here's how with the free WebSocket Tester, a working socket next to a failing one, and the proxy settings that usually fix it.
canonical: https://watchfor.io/blog/how-to-test-a-websocket
---

# Why won't my WebSocket connect? How to test it with the WebSocket Tester

2026-10-04 · WatchFor Team · Guides · guides, http, networking

Your WebSocket works on localhost and fails in production, and the browser console only says the connection failed. To test a WebSocket, open the connection from outside your network and look at the HTTP status of the upgrade: 101 means it works, anything else names the layer that refused it. Here's how with the free WebSocket Tester, a working socket next to a failing one, and the proxy settings that usually fix it.

To test a WebSocket, open a connection to its `ws://` or `wss://` URL from
outside your network and check the HTTP status of the upgrade request. A
working socket answers `101 Switching Protocols`. Any other status, such as
`200`, `400`, `403` or `502`, means something between the client and your
app answered the request as ordinary HTTP instead of upgrading it, and the
status tells you which layer to look at.

The free [WebSocket Tester](/websocket-tester) opens the connection from a
WatchFor probe location, times the handshake, measures a ping/pong round
trip and can send a message and show the first reply. No signup.

## What this tool answers

*Can a client on the internet open this WebSocket, and does the server talk
back?* Reach for it when a chat, live dashboard or notification feed works
locally but not in production, after putting nginx, a load balancer or a
CDN in front of a socket server, or when users on one network report
disconnects. The browser console says "WebSocket connection failed"; this
shows the actual handshake result.

## A real example

### Testing a WebSocket when the browser only says "connection failed"

The typical report: the app connects to `wss://app.example/ws` in
development, and in production the console shows a failed connection with
no detail. Is the socket server down, or is something in front of it
eating the upgrade? We tested a known-good socket and a URL that isn't one,
to show both results side by side.

In the [WebSocket Tester](/websocket-tester) we entered
`echo.websocket.org`, a public echo server, with the message `hello from a
probe` and **Measure ping/pong round-trip** ticked:

![The WebSocket Tester connected to wss://echo.websocket.org from Vilnius: 170 ms handshake, CONNECTED, ping 40 ms, server IP 66.241.124.119, 18 bytes sent, and the first message received, 32 bytes: Request served by 4d896d95b55478](/blog/how-to-test-a-websocket-connected.webp)

**Connected.** The handshake (DNS, TCP, TLS and the HTTP upgrade together)
took 170 ms, a WebSocket ping came back as a pong in 40 ms, and our 18-byte
message was sent. The first message the server sent back was not our echo
but a greeting, `Request served by 4d896d95b55478`. Many servers say hello
first, and the tester shows the first frame it receives.

Then `wss://example.com/`, an ordinary website with no socket endpoint:

![The WebSocket Tester result for wss://example.com/: FAILED, with the error Handshake failed with status 200: websocket: bad handshake](/blog/how-to-test-a-websocket-failed.webp)

**Failed: "Handshake failed with status 200: websocket: bad handshake."**
The server answered the upgrade request with a normal web page. That's the
same result you get when a reverse proxy in front of your socket server
doesn't pass the `Upgrade` header on: the request reaches a web handler,
which returns HTML.

**The answer:** if your URL returns 101 here, the socket server and
everything in front of it are fine, and the problem is in the client (the
URL it builds, cookies or tokens it doesn't send). If you get a status
instead, fix the layer that answered (table below). For nginx, the missing
lines are usually these:

```nginx
location /ws/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}
```

`Upgrade` and `Connection` are hop-by-hop headers, so nginx drops them
unless you set them explicitly, and the default `proxy_read_timeout` of 60
seconds closes idle sockets.

## How to read the results

| What you see | What it means |
| --- | --- |
| **Handshake (ms)** | Time from the start until the upgrade succeeded: DNS, TCP, TLS and the HTTP round trip. Green under 300 ms, amber under 1 s, red above. |
| **CONNECTED / FAILED** | Whether the server answered `101` and the connection opened. |
| **ping N ms** | Round trip of a WebSocket ping frame. A missing pong means the server or a proxy doesn't answer control frames. |
| **legacy TLS** (amber) | The `wss://` handshake only worked with old cipher suites. |
| **Server IP** | The address the probe actually connected to. Compare it with your load balancer or CDN. |
| **Sent** | Bytes of your message, sent as one text frame right after the handshake. |
| **First message received** | The first frame from the server, whatever it is: a greeting, your echo or an error. |
| **reply matched / did not match** | Shown when you fill **Expect in reply**: a regular expression tested against that first frame only. |
| **Error text** | The failure. `status 200` or `404`: the request hit a web route, not the socket. `400`: upgrade headers lost or rejected. `401`/`403`: auth or an origin check. `502`/`504`: the proxy couldn't reach the socket backend. A timeout or `connection refused`: nothing listening, or a firewall. |

## More ways to use it

- **After adding a load balancer or CDN.** Some need WebSockets switched on
  per route; a 400 or 426 here is the usual sign.
- **Checking idle timeouts.** If connections drop after a fixed time,
  compare that time with your proxy's read timeout.
- **Testing a staging socket from outside the VPN.** It shows whether the
  endpoint is reachable from the public internet at all.
- **Smoke test after a deploy.** Send the message your client sends first
  and put a word from the expected reply in **Expect in reply**.

## Tips and gotchas

- **The expect pattern sees one frame.** If your server greets first, as
  echo.websocket.org does, match the greeting or leave the field empty.
- **No custom headers or auth.** The tester sends no cookies, tokens or
  `Origin` header. A socket that requires them will fail here with 401, 403
  or a close right after connecting; that confirms the transport works up to
  the auth step.
- **IPv4 only.** The probe connects over IPv4; test IPv6 separately with
  curl.
- **`https://` is turned into `wss://`.** Paste either. A `ws://` URL is
  unencrypted, and browsers block it from an HTTPS page as
  [mixed content](/blog/mixed-content-warnings).
- **Ten seconds in total.** The handshake gets 5 seconds and the whole test
  10; a server that answers more slowly reads as a timeout.

## Check it from the command line too

curl can do the handshake itself. Send the upgrade headers over HTTP/1.1
and read the status line:

```bash
curl -si --http1.1 https://echo.websocket.org/ \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  --max-time 3 | head -4
```

```text
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```

That `sec-websocket-accept` value is the one RFC 6455 gives for this sample
key, so the server computed it correctly. The same request to
`https://example.com/` returns `HTTP/1.1 200 OK`, exactly what the tester
reported. Recent curl builds also speak WebSocket directly (support is on
by default since curl 8.11), and `curl wss://echo.websocket.org` prints what
the server sends:

```text
Request served by 4d896d95b55478
```

## Keep watching it

Sockets fail quietly: a config reload drops the upgrade headers, a
certificate on the socket host expires, a backend stops answering pings. A
[WebSocket monitor](/websocket-monitoring) opens the connection on a
schedule from the locations you choose, alerts when the upgrade fails, and can
alert on a slow handshake, a high ping round trip or a reply that stops
matching your pattern.

## FAQ

**How do I test if a WebSocket is working?**
Open a connection from outside your network and check that the upgrade
returns 101, then send a message and look at the reply. The free WebSocket
Tester does this without writing a client; `curl` with the upgrade headers
shows the status line.

**Why does my WebSocket work locally but not in production?**
Usually a reverse proxy or load balancer between the client and your app
doesn't forward the `Upgrade` and `Connection` headers, or closes idle
connections. The status in the handshake error tells you which layer
answered.

**What does "bad handshake" mean?**
The server answered the upgrade with something other than `101 Switching
Protocols`, often a 200 web page, a 400 or a 404. The request reached HTTP
handling instead of your socket endpoint. [Reverse proxies
explained](/blog/reverse-proxy-explained) covers where that happens.

---

Canonical page: https://watchfor.io/blog/how-to-test-a-websocket · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
