Debugging WebSocket Connections
WebSocket connections are stateful, long-lived, and bidirectional, which makes them fundamentally harder to debug than standard HTTP requests. You cannot simply replay a WebSocket session the way you would retry a failed GET request, and the persistent connection means problems can surface minutes or hours after the initial handshake. This article walks through every major tool and technique for tracking down WebSocket bugs, from browser DevTools to packet captures.
Browser DevTools
Chrome DevTools is the first place most developers look when a WebSocket connection misbehaves, and for good reason. The Network panel has built-in support for inspecting WebSocket frames in real time.
Opening the WebSocket Inspector
- Open DevTools with
Ctrl+Shift+I(Windows/Linux) orCmd+Option+I(macOS). - Go to the Network tab.
- Click the WS filter button to show only WebSocket connections.
- Initiate your WebSocket connection. You should see an entry appear with a status of
101 Switching Protocols. - Click on the connection to open the detail pane, then select the Messages tab.
The Messages tab shows each frame sent and received, color-coded by direction. Green arrows indicate outgoing frames, and white arrows indicate incoming frames. You can click on any frame to see its full payload.
Filtering and Searching Frames
When your WebSocket connection is chatty, scrolling through hundreds of frames is impractical. Use the filter input at the top of the Messages pane to search frame content. For example, if your protocol uses JSON, you can type a key name like "type":"error" to find error messages quickly.
You can also use the Console to monitor frames programmatically:
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (data) {
console.log('[WS Send]', data);
return originalSend.call(this, data);
};
This monkey-patch logs every outgoing message to the console. Pair it with an onmessage listener on your socket instance to capture incoming messages too.
Inspecting the Handshake
Click on the Headers tab of the WebSocket entry to see the HTTP upgrade request and response. Pay attention to these headers:
Sec-WebSocket-KeyandSec-WebSocket-Acceptconfirm the handshake completed correctly.Sec-WebSocket-Protocolshows which subprotocol was negotiated.Sec-WebSocket-Extensionsindicates whether compression (e.g.,permessage-deflate) is active.
If the handshake fails, the status code will not be 101. A 403 typically means an origin or authentication problem. A 400 can indicate a malformed upgrade request. Check the WebSocket Close Codes reference for help interpreting disconnection reasons.
Chrome Extensions for WebSocket Debugging
Several Chrome extensions provide additional WebSocket inspection capabilities beyond what DevTools offers natively.
The WebSocket Proxy Testing Tool extension lets you intercept, modify, and replay WebSocket messages without changing your application code. This is particularly useful when you need to simulate edge cases like malformed server responses or inject delays into message delivery. You can configure rules to match specific message patterns and transform payloads on the fly.
Other extensions worth trying include WebSocket King Client, which provides a standalone client for testing endpoints, and Smart WebSocket Client. These tools are helpful when you want to connect to a WebSocket server outside the context of your application, for example to verify that the server behaves correctly before debugging your client code.
For a broader look at testing approaches, see WebSocket Testing Tools.
Command-Line Debugging
Sometimes you want to test a WebSocket endpoint in isolation, without a browser. Command-line tools are perfect for this.
wscat
wscat is a WebSocket client built on Node.js. Install it globally and connect to any endpoint:
npm install -g wscat
wscat -c ws://localhost:8080/socket
Once connected, you can type messages directly and see server responses:
Connected (press CTRL+C to quit)
> {"type": "ping"}
< {"type": "pong", "timestamp": 1707753600}
You can also pass custom headers for authentication:
wscat -c wss://api.example.com/ws -H "Authorization: Bearer eyJhbGciOi..."
websocat
websocat is a more powerful alternative written in Rust. It supports piping, connecting two WebSocket endpoints together, and acting as a server:
# Connect and send a single message
echo '{"action": "subscribe", "channel": "trades"}' | websocat ws://localhost:8080/ws
# Pipe a file of messages, one per line
websocat ws://localhost:8080/ws < messages.txt
# Create a local WebSocket server that echoes messages
websocat -s 8765
websocat is especially useful for scripting automated tests against your WebSocket server. You can write a shell script that sends a sequence of messages and validates responses.
curl for the Handshake
While curl cannot maintain a WebSocket connection, it can test the HTTP upgrade handshake:
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
http://localhost:8080/socket
If the server responds with 101 Switching Protocols, you know the upgrade path is working. This is a quick way to rule out handshake-level problems.
Wireshark for WebSocket Traffic
When you need to see exactly what is happening on the wire, Wireshark is the definitive tool. It captures and decodes WebSocket frames at the packet level.
Capture Filters
To capture only WebSocket-relevant traffic, set a capture filter for the port your server runs on:
tcp port 8080
Display Filters
After capturing, use Wireshark display filters to isolate WebSocket frames:
websocket
You can filter further by opcode to find specific frame types:
websocket.opcode == 1 # Text frames
websocket.opcode == 2 # Binary frames
websocket.opcode == 8 # Close frames
websocket.opcode == 9 # Ping frames
websocket.opcode == 10 # Pong frames
Inspecting Payloads
Click on a WebSocket frame in the packet list and expand the WebSocket section in the packet detail pane. Wireshark shows:
- The opcode (text, binary, ping, pong, close)
- The masking key (client-to-server frames are always masked per RFC 6455)
- The unmasked payload data
- Whether the FIN bit is set (indicating the final fragment)
For TLS-encrypted connections (wss://), you need to configure Wireshark with the server’s private key or use the SSLKEYLOGFILE environment variable so the browser exports session keys. Without decryption, you will only see encrypted bytes.
# Set in your terminal before launching Chrome
export SSLKEYLOGFILE=~/Desktop/sslkeys.log
open -a "Google Chrome"
Then in Wireshark, go to Preferences > Protocols > TLS and set the (Pre)-Master-Secret log filename to the same file.
Proxy Tools
HTTP debugging proxies can also intercept WebSocket traffic, which is useful for modifying messages in transit or simulating network conditions.
mitmproxy
mitmproxy is an open-source proxy that supports WebSocket inspection. Start it and configure your browser or application to use it as an HTTP proxy:
mitmproxy --mode regular --listen-port 8888
WebSocket connections appear in the flow list. Select a flow and press Enter to see individual messages. You can also write Python scripts to modify messages programmatically:
from mitmproxy import ctx
def websocket_message(flow):
message = flow.websocket.messages[-1]
ctx.log.info(f"{'Client' if message.from_client else 'Server'}: {message.text}")
Save this as ws_logger.py and run mitmproxy with the script:
mitmproxy -s ws_logger.py
Charles Proxy and Fiddler
Charles Proxy (macOS/Windows) and Fiddler (Windows) are commercial tools that display WebSocket frames in their session views. Charles shows WebSocket messages under the connection entry in the Structure or Sequence view. Fiddler requires the WebSocket extension but then provides a similar frame-level view. Both support SSL proxying for wss:// connections after you install their root certificate.
Common Connection Issues and Fixes
Connection Refused
If your client gets ERR_CONNECTION_REFUSED, the WebSocket server is not listening on the expected host and port. Verify the server is running:
# Check if anything is listening on port 8080
lsof -i :8080 # macOS/Linux
netstat -an | findstr 8080 # Windows
Also confirm the URL scheme matches. If the server uses TLS, the client must connect with wss://, not ws://.
403 During Handshake
A 403 Forbidden during the upgrade request usually means the server is rejecting the Origin header. Many WebSocket servers validate the origin to prevent cross-site WebSocket hijacking. Check that your client’s origin matches the server’s allowlist. In development, you might need to configure the server to accept http://localhost:3000 or disable origin checking entirely.
If you are behind an API gateway or reverse proxy (e.g., nginx, AWS ALB), the proxy might strip or rewrite headers. Make sure the Upgrade and Connection headers are forwarded correctly.
Connection Drops After 60 Seconds
This is one of the most common WebSocket problems and it almost always comes from an intermediary, not your application. Load balancers and reverse proxies often have an idle timeout (frequently 60 seconds) that closes connections with no traffic.
The fix is to implement ping/pong frames. Most WebSocket server libraries support this natively:
// Node.js with the 'ws' library
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
ws.on('close', () => clearInterval(interval));
});
On the client side, browsers handle pong responses automatically. But if your proxy requires application-level pings (as opposed to WebSocket-level ping frames), you need to send periodic messages in your application protocol.
For nginx, you can also increase the proxy timeout:
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Messages Not Arriving
If messages are sent but not received, check for these causes:
- Buffering in the proxy layer. Some reverse proxies buffer responses by default. For nginx, add
proxy_buffering off;to your WebSocket location block. - Backpressure. If the server sends faster than the client can consume, messages may queue up. Monitor the
bufferedAmountproperty on the client-side WebSocket object. If it grows continuously, the client is not keeping up. - Message size limits. Many servers and proxies enforce a maximum frame size. If a message exceeds this limit, it might be silently dropped or the connection might close. Check the WebSocket protocol overview for details on fragmentation.
// Monitor bufferedAmount on the client
const ws = new WebSocket('ws://localhost:8080');
setInterval(() => {
console.log('Buffered bytes:', ws.bufferedAmount);
}, 1000);
Close Code 1006 (Abnormal Closure)
Close code 1006 means the connection was closed without a proper close frame. You will never see this code in an actual close frame because it is reserved for indicating that the TCP connection dropped unexpectedly. Common causes include:
- Network interruption (Wi-Fi switch, VPN disconnect)
- Server crash or forced restart
- Proxy or load balancer terminating the connection
- Client navigating away from the page
When you see 1006, focus on the network layer rather than your application logic. Check server logs for crash reports, and monitor your infrastructure for connection resets. See the Close Codes reference for the full list of standard codes and their meanings.
Server-Side Logging Strategies
Good server-side logging is the foundation of WebSocket debugging. Unlike HTTP where each request-response pair is a discrete unit, WebSocket connections span many events over time. Structure your logs around the connection lifecycle.
Log the Connection Lifecycle
At minimum, log these events with a connection identifier:
const { v4: uuidv4 } = require('uuid');
wss.on('connection', (ws, req) => {
const connId = uuidv4();
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
console.log(JSON.stringify({
event: 'ws_connect',
connId,
clientIp,
origin: req.headers.origin,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
}));
ws.on('message', (data) => {
console.log(JSON.stringify({
event: 'ws_message',
connId,
direction: 'incoming',
size: data.length,
timestamp: new Date().toISOString()
}));
});
ws.on('close', (code, reason) => {
console.log(JSON.stringify({
event: 'ws_close',
connId,
code,
reason: reason.toString(),
timestamp: new Date().toISOString()
}));
});
ws.on('error', (err) => {
console.error(JSON.stringify({
event: 'ws_error',
connId,
error: err.message,
timestamp: new Date().toISOString()
}));
});
});
Using structured JSON logs makes it easy to filter by connection ID when tracing a single session through your log aggregation tool.
Track Connection Metrics
Beyond individual events, track aggregate metrics: total active connections, message throughput, average connection duration, and error rates. These metrics help you spot systemic issues before individual users report them.
let activeConnections = 0;
wss.on('connection', (ws) => {
activeConnections++;
console.log(`Active connections: ${activeConnections}`);
ws.on('close', () => {
activeConnections--;
console.log(`Active connections: ${activeConnections}`);
});
});
In production, export these numbers to a monitoring system like Prometheus, Datadog, or Grafana so you can set alerts on connection spikes or drops.
Client-Side Logging and Error Reporting
On the client side, wrap your WebSocket in a class that logs events and reports errors to your monitoring service.
class DebugWebSocket {
constructor(url, protocols) {
this.url = url;
this.ws = new WebSocket(url, protocols);
this.messageCount = 0;
this.connectedAt = null;
this.ws.onopen = () => {
this.connectedAt = Date.now();
console.log(`[WS] Connected to ${url}`);
};
this.ws.onclose = (event) => {
const duration = this.connectedAt
? ((Date.now() - this.connectedAt) / 1000).toFixed(1)
: 'unknown';
console.log(
`[WS] Closed: code=${event.code} reason="${event.reason}" clean=${event.wasClean} duration=${duration}s messages=${this.messageCount}`
);
};
this.ws.onerror = (event) => {
console.error('[WS] Error:', event);
// Send to your error tracking service
// errorTracker.capture('websocket_error', { url, ... });
};
this.ws.onmessage = (event) => {
this.messageCount++;
};
}
send(data) {
if (this.ws.readyState !== WebSocket.OPEN) {
console.warn(`[WS] Cannot send, readyState is ${this.ws.readyState}`);
return false;
}
this.ws.send(data);
return true;
}
close(code, reason) {
this.ws.close(code, reason);
}
}
This wrapper gives you visibility into connection duration, message counts, and close reasons without cluttering your application logic.
Reconnection with Backoff
When debugging intermittent disconnects, add automatic reconnection with exponential backoff and log each attempt:
function connectWithRetry(url, attempt = 0) {
const ws = new WebSocket(url);
const maxDelay = 30000;
const delay = Math.min(1000 * Math.pow(2, attempt), maxDelay);
ws.onopen = () => {
console.log(`[WS] Connected after ${attempt} retries`);
attempt = 0;
};
ws.onclose = (event) => {
if (!event.wasClean) {
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${attempt + 1})`);
setTimeout(() => connectWithRetry(url, attempt + 1), delay);
}
};
return ws;
}
The retry logs help you determine whether disconnects are one-off blips or a recurring pattern.
FAQ
How do I debug WebSocket connections on mobile devices?
For Android, use Chrome’s chrome://inspect to remotely debug WebSocket connections in Chrome for Android. Connect your device via USB, enable USB debugging, then open chrome://inspect on your desktop Chrome. You will see all open tabs and can inspect WebSocket traffic the same way you would on desktop. For iOS, use Safari’s Web Inspector by enabling it in Settings > Safari > Advanced > Web Inspector, then connecting to your Mac and opening Safari’s Develop menu.
Why does my WebSocket connection work locally but fail in production?
The most common cause is a reverse proxy or load balancer that does not forward the Upgrade and Connection headers. Verify your proxy configuration passes these headers through. Another frequent cause is TLS certificate issues: your client uses wss:// in production, and the certificate might not cover the WebSocket endpoint’s domain or subdomain. Finally, check if a Web Application Firewall (WAF) is blocking the upgrade request.
Can I inspect WebSocket traffic in Firefox DevTools?
Yes. Firefox DevTools shows WebSocket connections in the Network panel, similar to Chrome. Click the WS filter, select a connection, and switch to the Messages tab to see individual frames. Firefox also shows the frame size and timestamp for each message. The Firefox DevTools documentation covers additional features.
How do I test WebSocket reconnection logic?
The simplest approach is to kill the server while a client is connected and observe the client’s behavior. For more controlled testing, use a tool like tc (traffic control) on Linux to simulate network interruptions, or use the WebSocket Proxy Testing Tool to inject connection drops at specific points. You can also call ws.close(1001, "going away") from the server to simulate a graceful shutdown and verify the client reconnects correctly.