
Websocket Realtime
- 27 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
websocket-realtime is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- websocket-realtime
- AI & Agent Building
- AI-coding skill
Websocket Realtime by the numbers
- 27 all-time installs (skills.sh)
- Ranked #9,601 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill websocket-realtimeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 27 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Websocket Realtime
Identity
I am a real-time systems architect who has built chat systems, collaborative editors, live dashboards, and multiplayer games. I've seen WebSocket connections drop, reconnection storms take down servers, and presence systems go stale.
My philosophy:
- Real-time is harder than it looks - plan for failure
- Every connection can drop at any moment
- Scaling WebSockets is fundamentally different from scaling HTTP
- Client and server must agree on message formats and semantics
- Presence and sync state are distributed systems problems
I help you build reliable real-time systems that survive the real world.
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
WebSocket & Real-time
Patterns
---
Name
Connection Lifecycle Management
Description
Properly handle the WebSocket connection states: connecting, open, closing, closed. Each state requires different handling.
Example
class WebSocketClient { private ws: WebSocket | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private reconnectDelay = 1000;
connect(url: string) { this.ws = new WebSocket(url);
this.ws.onopen = () => { console.log('Connected'); this.reconnectAttempts = 0; this.startHeartbeat(); };
this.ws.onclose = (event) => { this.stopHeartbeat(); if (!event.wasClean) { this.scheduleReconnect(); } };
this.ws.onerror = (error) => { console.error('WebSocket error:', error); };
this.ws.onmessage = (event) => { this.handleMessage(JSON.parse(event.data)); }; }
private scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnect attempts reached'); return; }
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts); this.reconnectAttempts++;
setTimeout(() => this.connect(this.url), delay); } }
When
Any WebSocket implementation
---
Name
Heartbeat/Ping-Pong
Description
Keep connections alive and detect stale connections with periodic heartbeats. Essential for detecting zombie connections.
Example
// Server-side (Node.js with ws) const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => { ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; }); });
// Ping all clients every 30 seconds const interval = setInterval(() => { wss.clients.forEach((ws) => { if (!ws.isAlive) { return ws.terminate(); } ws.isAlive = false; ws.ping(); }); }, 30000);
wss.on('close', () => { clearInterval(interval); });
// Client-side custom heartbeat class HeartbeatClient { private heartbeatInterval: number | null = null; private missedHeartbeats = 0; private maxMissedHeartbeats = 3;
startHeartbeat() { this.heartbeatInterval = setInterval(() => { if (this.missedHeartbeats >= this.maxMissedHeartbeats) { this.ws.close(); return; }
this.missedHeartbeats++; this.send({ type: 'ping' }); }, 10000); }
handleMessage(msg) { if (msg.type === 'pong') { this.missedHeartbeats = 0; } } }
When
Any persistent WebSocket connection
---
Name
Room/Channel Management
Description
Organize connections into rooms or channels for targeted broadcasting. Users subscribe to specific topics and receive only relevant messages.
Example
// Socket.IO room management io.on('connection', (socket) => { // Join a room socket.on('join', (roomId) => { socket.join(roomId); socket.to(roomId).emit('user-joined', socket.id); });
// Leave a room socket.on('leave', (roomId) => { socket.leave(roomId); socket.to(roomId).emit('user-left', socket.id); });
// Send to specific room socket.on('message', ({ roomId, content }) => { io.to(roomId).emit('message', { sender: socket.id, content, timestamp: Date.now() }); }); });
// Custom room implementation class RoomManager { private rooms = new Map<string, Set<WebSocket>>();
join(roomId: string, ws: WebSocket) { if (!this.rooms.has(roomId)) { this.rooms.set(roomId, new Set()); } this.rooms.get(roomId)!.add(ws); }
leave(roomId: string, ws: WebSocket) { this.rooms.get(roomId)?.delete(ws); }
broadcast(roomId: string, message: any, exclude?: WebSocket) { const room = this.rooms.get(roomId); if (!room) return;
const data = JSON.stringify(message); room.forEach((ws) => { if (ws !== exclude && ws.readyState === WebSocket.OPEN) { ws.send(data); } }); } }
When
Multi-user features, chat rooms, collaborative editing
---
Name
Message Protocol Design
Description
Define a clear message format with types, payloads, and optional request/response correlation for bidirectional communication.
Example
// Message envelope structure interface Message<T = unknown> { type: string; // Message type for routing id?: string; // Correlation ID for request/response payload: T; // Actual data timestamp: number; // Server timestamp version?: number; // Protocol version }
// Message types type ClientMessage = | { type: 'subscribe'; payload: { channel: string } } | { type: 'unsubscribe'; payload: { channel: string } } | { type: 'message'; payload: { channel: string; content: string } } | { type: 'ping'; payload: {} };
type ServerMessage = | { type: 'subscribed'; payload: { channel: string } } | { type: 'message'; payload: { channel: string; content: string; sender: string } } | { type: 'presence'; payload: { channel: string; users: string[] } } | { type: 'error'; payload: { code: string; message: string } } | { type: 'pong'; payload: {} };
// Server message handler function handleMessage(ws: WebSocket, raw: string) { let message: ClientMessage; try { message = JSON.parse(raw); } catch { ws.send(JSON.stringify({ type: 'error', payload: { code: 'INVALID_JSON', message: 'Invalid JSON' } })); return; }
switch (message.type) { case 'subscribe': handleSubscribe(ws, message.payload); break; case 'message': handleChatMessage(ws, message.payload); break; // ... } }
When
Any WebSocket application
---
Name
Presence System
Description
Track online/offline status of users with proper handling of disconnections, reconnections, and stale sessions.
Example
class PresenceManager { private presence = new Map<string, { status: 'online' | 'away' | 'offline'; lastSeen: number; connections: Set<string>; }>();
private cleanupInterval: NodeJS.Timer;
constructor() { // Clean up stale presence every minute this.cleanupInterval = setInterval( () => this.cleanupStale(), 60000 ); }
connect(userId: string, connectionId: string) { if (!this.presence.has(userId)) { this.presence.set(userId, { status: 'online', lastSeen: Date.now(), connections: new Set() }); }
const user = this.presence.get(userId)!; user.connections.add(connectionId); user.status = 'online'; user.lastSeen = Date.now();
this.broadcastPresence(userId); }
disconnect(userId: string, connectionId: string) { const user = this.presence.get(userId); if (!user) return;
user.connections.delete(connectionId);
// User still has other connections if (user.connections.size > 0) { return; }
// Delay offline status for reconnection window setTimeout(() => { const current = this.presence.get(userId); if (current && current.connections.size === 0) { current.status = 'offline'; this.broadcastPresence(userId); } }, 5000); }
private cleanupStale() { const staleThreshold = Date.now() - 5 60 1000; // 5 minutes
this.presence.forEach((user, userId) => { if (user.lastSeen < staleThreshold && user.connections.size === 0) { this.presence.delete(userId); } }); } }
When
User online status, typing indicators, collaborative features
---
Name
Server-Sent Events (SSE)
Description
Use SSE for server-to-client unidirectional streaming. Simpler than WebSocket when you don't need client-to-server messages.
Example
// Server (Express) app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive');
// Send initial data res.write(data: ${JSON.stringify({ type: 'connected' })}\n\n);
// Keep connection alive const heartbeat = setInterval(() => { res.write(': heartbeat\n\n'); }, 30000);
// Subscribe to events const handler = (event: any) => { res.write(event: ${event.type}\n); res.write(data: ${JSON.stringify(event.data)}\n\n); };
eventEmitter.on('notification', handler);
// Cleanup on disconnect req.on('close', () => { clearInterval(heartbeat); eventEmitter.off('notification', handler); }); });
// Client const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Message:', data); };
eventSource.addEventListener('notification', (event) => { const data = JSON.parse(event.data); showNotification(data); });
eventSource.onerror = () => { console.log('SSE connection error, will auto-reconnect'); };
When
Notifications, live feeds, one-way data streaming
---
Name
Scaling with Redis Pub/Sub
Description
Scale WebSocket servers horizontally using Redis for cross-instance message broadcasting.
Example
import Redis from 'ioredis';
const pub = new Redis(); const sub = new Redis();
class ScalableWebSocketServer { private wss: WebSocket.Server; private channels = new Map<string, Set<WebSocket>>();
constructor(port: number) { this.wss = new WebSocket.Server({ port });
// Subscribe to Redis channels sub.psubscribe('channel:*'); sub.on('pmessage', (pattern, channel, message) => { const channelName = channel.replace('channel:', ''); this.localBroadcast(channelName, JSON.parse(message)); });
this.wss.on('connection', (ws) => { ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); this.handleMessage(ws, msg); }); }); }
subscribe(ws: WebSocket, channel: string) { if (!this.channels.has(channel)) { this.channels.set(channel, new Set()); } this.channels.get(channel)!.add(ws); }
// Broadcast via Redis to all instances broadcast(channel: string, message: any) { pub.publish(channel:${channel}, JSON.stringify(message)); }
// Local broadcast to connections on this instance private localBroadcast(channel: string, message: any) { const subs = this.channels.get(channel); if (!subs) return;
const data = JSON.stringify(message); subs.forEach((ws) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } }); } }
When
Multiple server instances, horizontal scaling
---
Name
Optimistic Updates with Reconciliation
Description
Apply changes immediately on client, then reconcile with server response. Handle conflicts gracefully.
Example
class RealtimeDocument { private localVersion = 0; private serverVersion = 0; private pendingChanges: Change[] = [];
applyLocalChange(change: Change) { // Apply immediately this.localVersion++; change.localVersion = this.localVersion;
this.applyChange(change); this.pendingChanges.push(change);
// Send to server this.ws.send(JSON.stringify({ type: 'change', payload: change, baseVersion: this.serverVersion })); }
handleServerMessage(msg: ServerMessage) { if (msg.type === 'change-accepted') { // Remove from pending this.pendingChanges = this.pendingChanges.filter( c => c.localVersion !== msg.localVersion ); this.serverVersion = msg.serverVersion; }
if (msg.type === 'change-rejected') { // Rollback and replay this.rollbackToServerState(msg.serverState); this.replayPendingChanges(); }
if (msg.type === 'remote-change') { // Transform pending changes against remote this.pendingChanges = this.pendingChanges.map( c => this.transform(c, msg.change) );
// Apply remote change this.applyChange(msg.change); this.serverVersion = msg.serverVersion; } } }
When
Collaborative editing, real-time sync with conflict resolution
Anti-Patterns
---
Name
No Reconnection Strategy
Description
Not handling connection drops and not implementing reconnection
Why
WebSocket connections WILL drop. Mobile networks, laptop sleep, server restarts, load balancer timeouts - all cause disconnections. Without reconnection, users see a broken app.
Instead
Implement exponential backoff reconnection:
- Start with short delay (1s)
- Double delay on each attempt (2s, 4s, 8s...)
- Cap at max delay (30s)
- Reset on successful connection
- Show connection status to user
---
Name
Unbounded Message Buffers
Description
Queueing messages without limits when connection is down
Why
If connection drops and you buffer all messages, memory grows unbounded. When connection restores, sending all buffered messages can overwhelm server or cause stale data issues.
Instead
- Set max buffer size
- Drop oldest messages when full
- Consider which messages are time-sensitive
- Maybe just resync state on reconnection instead of replaying
---
Name
No Message Validation
Description
Trusting client messages without validation
Why
Clients can send anything. Malformed JSON, wrong types, malicious payloads. Trusting client input causes crashes and security issues.
Instead
Validate every message:
- Parse JSON in try/catch
- Validate message schema (Zod, Joi)
- Authenticate message sender
- Rate limit per connection
- Sanitize any user-generated content
---
Name
Blocking Event Loop
Description
Doing heavy work in WebSocket message handlers
Why
Node.js event loop is single-threaded. Heavy computation in message handlers blocks all connections. Latency spikes, timeouts, dropped connections.
Instead
Keep handlers fast:
- Offload heavy work to worker threads
- Use message queues for processing
- Respond immediately, process async
- Set reasonable timeouts
---
Name
No Heartbeat
Description
Relying on TCP keepalive alone to detect dead connections
Why
TCP keepalive is too slow and unreliable. Proxies and load balancers may not forward keepalives. Dead connections stay "open" for minutes.
Instead
Implement application-level heartbeat:
- Server pings clients every 30s
- Client responds with pong
- Close connections that miss 2-3 heartbeats
- Client can also ping server
---
Name
Broadcasting to All Connections
Description
Sending every message to every connected client
Why
Doesn't scale. With 10,000 connections, every message causes 10,000 sends. Server CPU spikes, clients get irrelevant messages.
Instead
Use rooms/channels:
- Clients subscribe to relevant channels
- Broadcast only to channel subscribers
- Use pub/sub for multi-server broadcast
- Consider message filtering server-side
Websocket Realtime - Sharp Edges
Websocket No Reconnection
Id
websocket-no-reconnection
Summary
Connection drops and app just dies without reconnecting
Severity
critical
Situation
You implement WebSocket connection. It works in dev. In production, connections drop randomly - mobile networks, laptop sleep, server restarts. Your app just shows disconnected state forever.
Why
WebSocket connections are NOT permanent. They WILL drop due to:
- Network changes (wifi to cellular)
- Proxy/load balancer timeouts (often 60s)
- Server restarts/deployments
- Client sleep/background
- Internet hiccups
Without reconnection, your app is broken for every dropped connection.
Solution
Implement exponential backoff reconnection:
class ReconnectingWebSocket { private reconnectAttempts = 0; private maxReconnectAttempts = 10; private baseDelay = 1000; private maxDelay = 30000;
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.reconnectAttempts = 0; // Reset on success this.onConnect(); };
this.ws.onclose = (event) => { if (!event.wasClean) { this.scheduleReconnect(); } }; }
private scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.onMaxRetriesReached(); return; }
// Exponential backoff with jitter const delay = Math.min( this.baseDelay Math.pow(2, this.reconnectAttempts) + Math.random() 1000, this.maxDelay );
this.reconnectAttempts++; setTimeout(() => this.connect(), delay); } }
Symptoms
- "Connection lost" stays forever
- Works locally but fails in production
- Users report needing to refresh
- App dies after laptop sleep
Detection Pattern
new WebSocket\\([^)]+\\)(?![\\s\\S]{0,500}reconnect)
Websocket Thundering Herd
Id
websocket-thundering-herd
Summary
Server restart causes all clients to reconnect simultaneously
Severity
critical
Situation
Your WebSocket server restarts. All 10,000 clients try to reconnect at the exact same moment. Server is overwhelmed before it can fully start. Connections fail, clients retry, cycle repeats.
Why
Simultaneous reconnection creates a thundering herd:
- All clients use same backoff starting at 0
- Server can't handle 10K simultaneous handshakes
- Failed connections retry immediately
- Cascading failure ensues
Solution
Add randomized jitter to reconnection delay:
private scheduleReconnect() { // Base delay + exponential backoff let delay = this.baseDelay * Math.pow(2, this.attempts);
// Add significant random jitter (0-50% of delay) delay += Math.random() delay 0.5;
// Add initial random delay on first reconnect (0-5s) if (this.attempts === 0) { delay += Math.random() * 5000; }
setTimeout(() => this.connect(), delay); }
Server-side protection:
- Connection rate limiting per IP
- Gradual rollout of restarts
- Health check before accepting connections
- Queue connections during startup
Symptoms
- Server crashes after restart
- Connections fail in waves
- CPU spikes to 100% on restart
- Load balancer marks server unhealthy
Detection Pattern
setTimeout.connect.\\d{3,4}\\)
Websocket Memory Leak
Id
websocket-memory-leak
Summary
Event listeners accumulate causing memory leak and slowdown
Severity
critical
Situation
Your WebSocket client reconnects multiple times. Each reconnection adds new event listeners without cleaning up old ones. Memory grows, event handlers fire multiple times, app slows down.
Why
Each connection might add:
- onmessage handlers
- onclose handlers
- Custom event listeners
- Interval timers for heartbeat
Without cleanup, these accumulate across reconnections.
Solution
Clean up everything on disconnect:
class CleanWebSocket { private ws: WebSocket | null = null; private heartbeatInterval: number | null = null; private messageHandlers: Set<Function> = new Set();
connect() { // Clean up previous connection first this.cleanup();
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.startHeartbeat(); };
this.ws.onclose = () => { this.cleanup(); this.scheduleReconnect(); }; }
private cleanup() { // Stop heartbeat if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; }
// Close existing connection if (this.ws) { this.ws.onclose = null; // Prevent triggering handler this.ws.close(); this.ws = null; } }
// Use AbortController for fetch-based cleanup private controller: AbortController | null = null;
startSession() { this.controller?.abort(); // Cancel previous this.controller = new AbortController();
// All async operations use this signal } }
Symptoms
- Memory usage grows over time
- Events fire multiple times
- App gets slower after reconnections
- "Maximum call stack exceeded" errors
Detection Pattern
addEventListener(?![\\s\\S]{0,200}removeEventListener)
Websocket No Authentication
Id
websocket-no-authentication
Summary
WebSocket connection established without proper authentication
Severity
high
Situation
You implement WebSocket. HTTP endpoints are authenticated, but WebSocket just connects. Anyone with the URL can connect and receive all messages.
Why
WebSocket upgrade bypasses normal HTTP middleware. Authentication cookies might be sent, but you need to verify them. Without auth:
- Anyone can connect
- Can receive private messages
- Can send messages as anyone
- Resource exhaustion attacks
Solution
Authenticate on connection:
// Option 1: Token in query string const ws = new WebSocket(wss://api.example.com?token=${authToken});
// Server verification wss.on('connection', (ws, req) => { const url = new URL(req.url, 'http://localhost'); const token = url.searchParams.get('token');
try { const user = verifyToken(token); ws.userId = user.id; } catch { ws.close(4001, 'Unauthorized'); return; } });
// Option 2: First message authentication wss.on('connection', (ws) => { ws.authenticated = false;
ws.on('message', (data) => { const msg = JSON.parse(data);
if (!ws.authenticated) { if (msg.type === 'auth' && verifyToken(msg.token)) { ws.authenticated = true; ws.userId = msg.userId; } else { ws.close(4001, 'Unauthorized'); } return; }
// Only process messages after auth handleMessage(ws, msg); });
// Force close if not authenticated within 5s setTimeout(() => { if (!ws.authenticated) ws.close(4001, 'Auth timeout'); }, 5000); });
Symptoms
- Unauthenticated connections succeed
- Users see others' private messages
- Security audit failures
Detection Pattern
on\([ '"]connection[ '"].*\)(?![\s\S]{0,300}auth|token|verify)
Websocket No Rate Limiting
Id
websocket-no-rate-limiting
Summary
Clients can send unlimited messages overwhelming server
Severity
high
Situation
Your WebSocket server processes every message. A malicious or buggy client sends thousands of messages per second. Server CPU spikes, other clients experience delays, server may crash.
Why
Unlike HTTP, WebSocket has no built-in rate limiting. A single connection can flood the server with messages. No protection means:
- DoS from single client
- Resource exhaustion
- Cascade failures
- Other clients affected
Solution
Implement per-connection rate limiting:
class RateLimitedConnection { private messageCount = 0; private lastReset = Date.now(); private readonly maxMessages = 100; // per second private readonly windowMs = 1000;
handleMessage(ws: WebSocket, data: string) { // Reset counter if window passed const now = Date.now(); if (now - this.lastReset > this.windowMs) { this.messageCount = 0; this.lastReset = now; }
this.messageCount++;
if (this.messageCount > this.maxMessages) { ws.send(JSON.stringify({ type: 'error', code: 'RATE_LIMITED', message: 'Too many messages, slow down' }));
// Optionally disconnect repeat offenders if (this.messageCount > this.maxMessages * 2) { ws.close(4029, 'Rate limit exceeded'); } return; }
// Process message normally this.processMessage(ws, data); } }
Symptoms
- Single client causes server slowdown
- CPU spikes with many messages
- Server becomes unresponsive
- Other clients timeout
Detection Pattern
on\([ '"]message '"
Websocket Missing Error Handling
Id
websocket-missing-error-handling
Summary
Errors in message handlers crash server or leave connection hanging
Severity
high
Situation
Client sends malformed message. JSON.parse throws. Handler function throws. The error bubbles up, crashes the connection or even the server process.
Why
WebSocket message handlers run for every message. Any uncaught error:
- May crash Node.js process
- Leaves connection in bad state
- Other clients affected
- No error sent to client
Solution
Wrap all message handling in try-catch:
ws.on('message', async (raw) => { try { // Parse JSON safely let message; try { message = JSON.parse(raw.toString()); } catch { ws.send(JSON.stringify({ type: 'error', code: 'INVALID_JSON' })); return; }
// Validate message schema const result = messageSchema.safeParse(message); if (!result.success) { ws.send(JSON.stringify({ type: 'error', code: 'INVALID_MESSAGE', details: result.error.issues })); return; }
// Handle message await handleMessage(ws, result.data);
} catch (error) { console.error('Message handler error:', error);
ws.send(JSON.stringify({ type: 'error', code: 'INTERNAL_ERROR', message: 'Something went wrong' }));
// Don't crash - connection can continue } });
// Global error handler as safety net process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); // Graceful shutdown });
Symptoms
- Server crashes on bad input
- Connections die mysteriously
- No error messages to client
- Intermittent failures
Detection Pattern
on\([ '"]message[ '"].*JSON\.parse(?![\s\S]{0,100}catch)
Websocket Load Balancer Timeout
Id
websocket-load-balancer-timeout
Summary
Load balancer closes idle connections before app expects
Severity
high
Situation
WebSocket works in dev. In production behind load balancer, connections drop after 60 seconds of no messages. Users constantly disconnecting.
Why
Load balancers (ALB, nginx, CloudFlare) have idle timeouts:
- AWS ALB: 60 seconds default
- nginx: 60 seconds default
- CloudFlare: 100 seconds
If no data flows, load balancer assumes connection is dead and closes it.
Solution
Keep connections active with heartbeat:
// Server-side ping (every 30 seconds) const heartbeatInterval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); // Built-in ping frame } }); }, 30000);
// Client-side ping (for ALBs that don't forward ping frames) setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 30000);
// Also configure load balancer: // ALB: Increase idle timeout to 3600s // nginx: proxy_read_timeout 3600s;
Symptoms
- Connections drop after ~60 seconds idle
- Works in dev, fails in production
- Works during active use, fails when idle
Detection Pattern
WebSocket(?![\\s\\S]{0,500}ping|heartbeat)
Websocket No Offline Queue
Id
websocket-no-offline-queue
Summary
Messages sent while offline are lost forever
Severity
medium
Situation
User loses connection briefly. They send messages during disconnect. Messages are lost. Connection restores but sent messages are gone.
Why
send() on a closed WebSocket throws or silently fails. Without queuing, any message sent during disconnection is lost. Users don't know their action didn't go through.
Solution
Queue messages during disconnect:
class QueuedWebSocket { private queue: string[] = []; private maxQueueSize = 100;
send(message: any) { const data = JSON.stringify(message);
if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(data); } else { // Queue for later if (this.queue.length < this.maxQueueSize) { this.queue.push(data); } else { console.warn('Message queue full, dropping message'); } } }
private onConnect() { // Flush queued messages while (this.queue.length > 0) { const msg = this.queue.shift()!; this.ws!.send(msg); } } }
// Consider which messages should be queued: // - User actions: Queue // - Typing indicators: Drop // - Heartbeats: Drop
Symptoms
- Messages disappear during reconnection
- Users complain messages "didn't send"
- Actions need to be repeated
Detection Pattern
send\\([^)]+\\)(?![\\s\\S]{0,200}queue|buffer)
Websocket Presence Ghost Users
Id
websocket-presence-ghost-users
Summary
Users show as online when they've actually disconnected
Severity
medium
Situation
User closes browser tab. Your presence system still shows them online. Other users send messages expecting response. Ghost users everywhere.
Why
Browser tab close doesn't always send close frame. Network drops don't either. Your server thinks connection is alive when it's not. Without heartbeat-based cleanup, ghosts accumulate.
Solution
Use heartbeat to detect dead connections:
class PresenceWithCleanup { private userHeartbeats = new Map<string, number>();
onHeartbeat(userId: string) { this.userHeartbeats.set(userId, Date.now()); }
// Run every 30 seconds cleanupGhosts() { const now = Date.now(); const timeout = 60000; // 60 seconds
this.userHeartbeats.forEach((lastSeen, userId) => { if (now - lastSeen > timeout) { this.markOffline(userId); this.userHeartbeats.delete(userId); } }); }
// Server-side ping to detect dead connections pingAllConnections() { this.connections.forEach((ws, userId) => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); // If no pong received within 10s, close } }); } }
Symptoms
- Users show online for hours after leaving
- Ghost users in participant lists
- Messages sent to disconnected users
Detection Pattern
presence|online(?![\\s\\S]{0,300}heartbeat|cleanup|timeout)
Websocket No Backpressure
Id
websocket-no-backpressure
Summary
Server sends faster than client can process, causing memory issues
Severity
medium
Situation
Your server broadcasts high-frequency updates. Some clients on slow connections can't keep up. Server buffers messages, memory grows, eventually server or client crashes.
Why
WebSocket send() buffers if network can't keep up. Buffer grows unbounded. Server memory exhausted. Client drowns in messages when buffer finally flushes.
Solution
Monitor and handle backpressure:
// Check bufferedAmount before sending function safeSend(ws: WebSocket, data: string) { const MAX_BUFFER = 1024 * 1024; // 1MB
if (ws.bufferedAmount > MAX_BUFFER) { console.warn('Client buffer full, dropping message'); // Optionally mark client as slow return false; }
ws.send(data); return true; }
// For high-frequency updates, batch class BatchedBroadcast { private batch: any[] = []; private batchInterval = 100; // ms
add(message: any) { this.batch.push(message); }
start() { setInterval(() => { if (this.batch.length > 0) { const data = JSON.stringify(this.batch); this.broadcast(data); this.batch = []; } }, this.batchInterval); } }
Symptoms
- Server memory grows over time
- Slow clients cause issues
- Clients receive stale data in bursts
Detection Pattern
send\\([^)]+\\)(?![\\s\\S]{0,200}bufferedAmount)
Websocket Realtime - Validations
WebSocket Without Reconnection
Id
ws-no-reconnection
Severity
error
Type
regex
Pattern
- new WebSocket\([^)]+\)(?![\s\S]{0,500}reconnect)
- new WebSocket\([^)]+\)(?![\s\S]{0,500}onclose.*connect)
Message
WebSocket created without reconnection logic. Connections will stay dead after drops.
Fix Action
Implement exponential backoff reconnection in onclose handler
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
WebSocket Without Heartbeat
Id
ws-no-heartbeat
Severity
warning
Type
regex
Pattern
- new WebSocket\([^)]+\)(?![\s\S]{0,500}ping|heartbeat)
- WebSocket\.Server(?![\s\S]{0,500}ping)
Message
WebSocket without heartbeat. Idle connections may be dropped by proxies.
Fix Action
Add ping/pong every 30 seconds to keep connections alive
Applies To
- */.ts
- */.js
WebSocket Without Error Handler
Id
ws-no-error-handler
Severity
warning
Type
regex
Pattern
- new WebSocket\([^)]+\)(?![\s\S]{0,200}\.onerror)
- WebSocket\([^)]+\);(?![\s\S]{0,200}onerror)
Message
WebSocket without error handler. Errors will go unnoticed.
Fix Action
Add onerror handler to log and handle connection errors
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
WebSocket Without Authentication
Id
ws-no-auth
Severity
error
Type
regex
Pattern
- on\(['"]connection['"].*\)(?![\s\S]{0,300}auth|token|verify|jwt)
- wss\.on\(['"]connection'"
Message
WebSocket connection handler without authentication check.
Fix Action
Verify auth token on connection before allowing messages
Applies To
- */.ts
- */.js
WebSocket Without Rate Limiting
Id
ws-no-rate-limit
Severity
warning
Type
regex
Pattern
- on\(['"]message'"
Message
WebSocket message handler without rate limiting.
Fix Action
Add per-connection rate limiting to prevent abuse
Applies To
- */.ts
- */.js
Unsafe JSON.parse in Message Handler
Id
ws-unsafe-json-parse
Severity
warning
Type
regex
Pattern
- on\(['"]message['"].*JSON\.parse(?![\s\S]{0,100}try|catch)
- JSON\.parse\(.*\.data(?![\s\S]{0,50}catch)
Message
JSON.parse without try-catch in message handler. Malformed messages will crash.
Fix Action
Wrap JSON.parse in try-catch and send error to client
Applies To
- */.ts
- */.js
No Message Schema Validation
Id
ws-no-message-validation
Severity
info
Type
regex
Pattern
- JSON\.parse\([^)]+\)(?![\s\S]{0,200}schema|validate|safeParse|parse\()
Message
Parsed message not validated against schema.
Fix Action
Validate message structure with Zod or similar before processing
Applies To
- */.ts
- */.js
Event Listener Without Cleanup
Id
ws-event-listener-leak
Severity
warning
Type
regex
Pattern
- addEventListener(?![\s\S]{0,500}removeEventListener)
- \.on\(['"]\w+'"
Message
Event listener added without corresponding removal. May cause memory leak.
Fix Action
Remove listeners in cleanup/disconnect handler
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Interval Without Cleanup
Id
ws-interval-not-cleared
Severity
warning
Type
regex
Pattern
- setInterval\([^)]+\)(?![\s\S]{0,300}clearInterval)
Message
setInterval without clearInterval. Will cause memory leak on disconnect.
Fix Action
Store interval ID and clear it in onclose/cleanup
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Send Without Buffer Check
Id
ws-no-buffer-check
Severity
info
Type
regex
Pattern
- \.send\([^)]+\)(?![\s\S]{0,200}bufferedAmount)
Message
WebSocket send without checking buffer. May cause backpressure issues.
Fix Action
Check ws.bufferedAmount before sending high-frequency messages
Applies To
- */.ts
- */.js
Broadcasting to All Connections
Id
ws-broadcast-all
Severity
info
Type
regex
Pattern
- \.clients\.forEach\([^)]*send
- connections\.forEach\([^)]*send
Message
Broadcasting to all connections. Consider using rooms/channels for efficiency.
Fix Action
Implement room-based broadcasting to reduce unnecessary messages
Applies To
- */.ts
- */.js
Send Without Ready State Check
Id
ws-send-without-ready-check
Severity
warning
Type
regex
Pattern
- \.send\([^)]+\)(?![\s\S]{0,100}readyState|OPEN)
Message
WebSocket send without checking readyState. Will throw if not open.
Fix Action
Check ws.readyState === WebSocket.OPEN before sending
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Close Without Status Code
Id
ws-close-code-missing
Severity
info
Type
regex
Pattern
- \.close\(\)(?!\d)
Message
WebSocket closed without status code. Consider providing meaningful close code.
Fix Action
Use appropriate close code: ws.close(1000, 'Normal closure')
Applies To
- */.ts
- */.js
Fixed Reconnection Delay
Id
ws-constant-reconnect-delay
Severity
info
Type
regex
Pattern
- setTimeout.connect.\d{4}\)
- reconnect.setTimeout.1000
Message
Fixed reconnection delay may cause thundering herd on server restart.
Fix Action
Use exponential backoff with jitter for reconnection
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Unlimited Reconnection Attempts
Id
ws-no-max-reconnect
Severity
info
Type
regex
Pattern
- reconnect(?![\s\S]{0,200}max|limit|attempts)
Message
Reconnection without max attempts. Client may retry forever.
Fix Action
Limit reconnection attempts and show user error after max
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Socket.IO Without Disconnect Handler
Id
socketio-no-disconnect-handler
Severity
warning
Type
regex
Pattern
- io\.on\(['"]connection'"
Message
Socket.IO connection without disconnect handler.
Fix Action
Add socket.on('disconnect') to clean up resources
Applies To
- */.ts
- */.js
Socket.IO Emit Without Acknowledgment
Id
socketio-emit-without-ack
Severity
info
Type
regex
Pattern
- \.emit\(['"][^'"]+['"],\s[^,)]+\)(?!.=>)
Message
Socket.IO emit without callback. Consider using ack for important messages.
Fix Action
Add callback for delivery confirmation: emit('event', data, (ack) => {})
Applies To
- */.ts
- */.js
SSE Without Retry Header
Id
sse-no-retry
Severity
info
Type
regex
Pattern
- text/event-stream(?![\s\S]{0,200}retry:)
Message
SSE without retry directive. Client will use default reconnection timing.
Fix Action
Set retry interval: res.write('retry: 3000\n')
Applies To
- */.ts
- */.js
SSE Without Keepalive
Id
sse-no-heartbeat
Severity
warning
Type
regex
Pattern
- text/event-stream(?![\s\S]{0,500}heartbeat|interval|setInterval)
Message
SSE without heartbeat. Connection may be dropped by proxies.
Fix Action
Send heartbeat comment every 30s: res.write(': heartbeat\n\n')
Applies To
- */.ts
- */.js