WebSocket connections are fundamentally different from stateless HTTP requests, and monitoring them requires a different mindset. Long-lived connections need special metrics that capture their unique behavior: connection lifecycle, message flow, and unexpected disconnections. We'll walk through the essential metrics and instrumentation patterns that help you keep WebSocket servers healthy and responsive.
Why WebSocket monitoring is different
Traditional HTTP monitoring focuses on request latency and error rates. WebSocket servers maintain persistent connections that can live for hours or days, and the problems that arise are often invisible to standard HTTP-centric observability.
A WebSocket server might report a 200 OK handshake, but if connections drop silently or fail to reconnect, your users experience cascading failures. You need metrics that track connection state continuously, not just at request boundaries. This is where gauges and histograms become essential rather than optional.
We've seen production incidents where a WebSocket server was consuming memory at a healthy rate, CPU looked fine, but connections were being held open indefinitely by stuck clients. The application layer appeared functional, but the real problem was connection churn hiding in the details.
Active connections gauge
The first and most critical metric is a gauge that tracks the number of active WebSocket connections at any moment. This is your health indicator—if it's trending upward without bound or dropping to zero unexpectedly, you know something's wrong.
Here's how you'd instrument this in a Node.js/NestJS application:
import { Gauge } from 'prom-client';
const activeConnections = new Gauge({
name: 'websocket_active_connections',
help: 'Number of active WebSocket connections',
labelNames: ['server', 'endpoint']
});
// In your connection handler
wss.on('connection', (ws, req) => {
const endpoint = req.url;
activeConnections.inc({ server: 'prod', endpoint });
ws.on('close', () => {
activeConnections.dec({ server: 'prod', endpoint });
});
ws.on('error', () => {
activeConnections.dec({ server: 'prod', endpoint });
});
});
With this gauge exposed, you can graph connection count over time and set alerts like "if active_connections drops below 100 for 2 minutes, page on-call." You can also break down connections by endpoint to identify which features are losing connections consistently.
In Grafana, a simple single-stat panel showing the current value lets you see at a glance whether your server has the expected number of clients connected right now. Pair it with a range graph below showing the last 24 hours to spot trends.
Messages in and out
Counting messages flowing through your WebSocket server tells you whether connections are actually working or just sitting idle. A connection with zero message traffic for hours is effectively dead, even if the TCP socket is still open.
Instrument both inbound and outbound messages with counters:
import { Counter } from 'prom-client';
const messagesIn = new Counter({
name: 'websocket_messages_in_total',
help: 'Total WebSocket messages received',
labelNames: ['server', 'endpoint', 'message_type']
});
const messagesOut = new Counter({
name: 'websocket_messages_out_total',
help: 'Total WebSocket messages sent',
labelNames: ['server', 'endpoint', 'message_type']
});
wss.on('connection', (ws, req) => {
const endpoint = req.url;
ws.on('message', (data) => {
let messageType = 'unknown';
try {
const parsed = JSON.parse(data);
messageType = parsed.type || 'unknown';
} catch (e) {
messageType = 'malformed';
}
messagesIn.inc({ server: 'prod', endpoint, message_type: messageType });
// Handle message...
});
// When you send a message back
ws.send(JSON.stringify({ type: 'update', payload: data }));
messagesOut.inc({ server: 'prod', endpoint, message_type: 'update' });
});
The message type label is crucial. It reveals whether clients are spamming certain message types (a sign of client bugs or DOS attempts) or whether a particular message handler is broken and clients keep retrying. We've caught several issues where a single bad deployment caused all clients to repeatedly send a heartbeat message at 10x normal rate.
Calculate the message throughput by taking the rate of these counters over 5 minutes. A sudden drop in throughput often precedes user complaints about features not working.
Connection duration
How long do connections typically live? A histogram of connection lifetimes helps you understand expected behavior and spot pathological patterns.
Track the time from connection open to close:
import { Histogram } from 'prom-client';
const connectionDuration = new Histogram({
name: 'websocket_connection_duration_seconds',
help: 'WebSocket connection duration',
buckets: [10, 30, 60, 300, 3600, 86400],
labelNames: ['server', 'endpoint', 'close_reason']
});
wss.on('connection', (ws, req) => {
const endpoint = req.url;
const startTime = Date.now();
const recordDuration = (reason) => {
const durationSec = (Date.now() - startTime) / 1000;
connectionDuration.observe(
{ server: 'prod', endpoint, close_reason: reason },
durationSec
);
};
ws.on('close', () => {
recordDuration('normal_close');
});
ws.on('error', (error) => {
recordDuration('error');
});
});
This histogram reveals whether connections are living as long as expected. In a real-time trading application we worked on, the 99th percentile of connection duration suddenly dropped from 8 hours to 15 minutes. This indicated that a network change was forcefully closing idle connections, and we needed to add a ping/pong handler to keep them alive.
The close_reason label is also valuable. Group metrics by reason to see if most closures are normal application logic, network errors, or client-initiated. A spike in error-triggered closures warrants investigation.
Reconnect rates
When clients reconnect rapidly, that's a signal. It could mean your server is crashing and restarting, or clients are implementing a problematic exponential backoff algorithm. Some reconnect churn is normal, but sustained high rates indicate trouble.
Track reconnections per client or session:
import { Counter } from 'prom-client';
const reconnectCounter = new Counter({
name: 'websocket_reconnect_attempts_total',
help: 'Total WebSocket reconnection attempts',
labelNames: ['server', 'endpoint', 'client_id']
});
// On your auth/handshake handler
function handleConnection(ws, req) {
const clientId = req.headers['x-client-id'];
const endpoint = req.url;
// Check if client previously connected
if (clientStore.has(clientId)) {
reconnectCounter.inc({ server: 'prod', endpoint, client_id: clientId });
}
clientStore.set(clientId, { ws, connectedAt: Date.now() });
}
Monitor the rate of reconnections using rate(websocket_reconnect_attempts_total[5m]). A threshold alert like "if reconnect rate exceeds 10 per second for 1 minute, investigate" catches both client library bugs and server instability.
We also recommend tracking the distribution of time between reconnection attempts. A histogram of inter-reconnection delays shows whether clients are backing off exponentially (healthy) or hammering in a tight loop (broken).
Common mistakes
Forgetting to decrement gauges on error: If your error handler doesn't decrement the active connections gauge, the metric becomes useless noise. Every code path that closes a connection must decrement.
Missing message type labels: Without breaking down message throughput by type, you can't diagnose which features are broken. A catch-all counter hides problems.
Not tracking close reasons: Recording connection duration but not why it closed misses the real story. Normal client logouts look identical to network errors in an unlabeled histogram.
Ignoring half-open connections: TCP can leave sockets half-open indefinitely. Add a heartbeat mechanism (WebSocket ping/pong) and track how many clients stop responding to pings. Without this, your gauge reports false positives.
Treating all endpoints the same: A spike in reconnects on one endpoint is a feature bug. Mixing all endpoints together masks problems. Always use endpoint labels.
Key takeaways
- Active connections gauge is your primary health indicator—monitor it for unexpected trends and alert on sudden drops.
- Message counters (in and out) with message type labels reveal which features are broken and whether clients are retrying pathologically.
- Connection duration histograms with close reason labels expose whether connections live as long as expected and why they're terminating.
- Reconnect rate tracking catches both client library bugs and server instability before they become widespread outages.
- Always use labels for server, endpoint, and close/message reason to avoid hiding problems in aggregate metrics.
- Pair your Prometheus metrics with alert rules that page on-call when reconnect rates spike or active connections drop unexpectedly.






Comments
Loading comments…