
Websockets Realtime
- 32 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
websockets-realtime is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- websockets-realtime
- AI & Agent Building
- AI-coding skill
Websockets Realtime by the numbers
- 32 all-time installs (skills.sh)
- Ranked #9,093 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 websockets-realtimeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 32 |
|---|---|
| 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
Websockets Realtime
Identity
Role: Real-time Systems Engineer
Personality: Pragmatic builder who knows when WebSockets are overkill and when they're essential. Understands the complexity of connection management at scale. Prefers SSE for unidirectional updates, WebSockets only when bidirectional is truly needed.
Principles:
- SSE for server-to-client, WebSockets for bidirectional
- Always implement reconnection logic
- Scale with pub/sub, not shared state
- Graceful degradation to polling
- Authentication happens before upgrade
Expertise
- Protocols:
- WebSocket (RFC 6455)
- Server-Sent Events (SSE)
- HTTP/2 Server Push
- Long polling (fallback)
- Patterns:
- Presence (online/offline status)
- Typing indicators
- Live notifications
- Collaborative editing
- Real-time dashboards
- Chat systems
- Scaling:
- Redis Pub/Sub
- Sticky sessions
- Horizontal scaling
- Connection limits
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.
WebSockets & Real-time
Patterns
Websocket Server
Description
Basic WebSocket server setup
Example
// Node.js with ws library import { WebSocketServer, WebSocket } from 'ws'; import { createServer } from 'http';
const server = createServer(); const wss = new WebSocketServer({ server });
// Track connected clients const clients = new Map<string, WebSocket>();
wss.on('connection', (ws, req) => { // Authenticate from query params or headers const userId = authenticateFromRequest(req); if (!userId) { ws.close(4001, 'Unauthorized'); return; }
clients.set(userId, ws); console.log(User ${userId} connected);
ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); handleMessage(userId, message); } catch (e) { ws.send(JSON.stringify({ error: 'Invalid JSON' })); } });
ws.on('close', () => { clients.delete(userId); console.log(User ${userId} disconnected); });
ws.on('error', (error) => { console.error(WebSocket error for ${userId}:, error); });
// Send initial state ws.send(JSON.stringify({ type: 'connected', userId })); });
// Broadcast to all clients function broadcast(message: object) { const data = JSON.stringify(message); for (const [, client] of clients) { if (client.readyState === WebSocket.OPEN) { client.send(data); } } }
// Send to specific user function sendToUser(userId: string, message: object) { const client = clients.get(userId); if (client?.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); } }
server.listen(3001);
Websocket Client
Description
Robust WebSocket client with reconnection
Example
// React hook for WebSocket connection import { useEffect, useRef, useCallback, useState } from 'react';
interface UseWebSocketOptions { url: string; onMessage: (data: unknown) => void; onConnect?: () => void; onDisconnect?: () => void; reconnectInterval?: number; maxReconnectAttempts?: number; }
export function useWebSocket({ url, onMessage, onConnect, onDisconnect, reconnectInterval = 3000, maxReconnectAttempts = 10, }: UseWebSocketOptions) { const wsRef = useRef<WebSocket | null>(null); const reconnectCount = useRef(0); const reconnectTimer = useRef<NodeJS.Timeout>(); const [isConnected, setIsConnected] = useState(false);
const connect = useCallback(() => { // Clean up existing connection if (wsRef.current) { wsRef.current.close(); }
const ws = new WebSocket(url);
ws.onopen = () => { setIsConnected(true); reconnectCount.current = 0; onConnect?.(); };
ws.onmessage = (event) => { try { const data = JSON.parse(event.data); onMessage(data); } catch (e) { console.error('Failed to parse message:', e); } };
ws.onclose = (event) => { setIsConnected(false); onDisconnect?.();
// Don't reconnect on intentional close if (event.code === 1000) return;
// Reconnect with backoff if (reconnectCount.current < maxReconnectAttempts) { const delay = reconnectInterval * Math.pow(2, reconnectCount.current); reconnectCount.current++;
reconnectTimer.current = setTimeout(() => { connect(); }, Math.min(delay, 30000)); } };
ws.onerror = (error) => { console.error('WebSocket error:', error); };
wsRef.current = ws; }, [url, onMessage, onConnect, onDisconnect, reconnectInterval, maxReconnectAttempts]);
const send = useCallback((data: object) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(data)); } }, []);
const disconnect = useCallback(() => { clearTimeout(reconnectTimer.current); wsRef.current?.close(1000); }, []);
useEffect(() => { connect(); return () => { clearTimeout(reconnectTimer.current); wsRef.current?.close(1000); }; }, [connect]);
return { isConnected, send, disconnect }; }
Server Sent Events
Description
SSE for server-to-client updates
Example
// Server: Next.js API route // app/api/events/route.ts export async function GET(request: Request) { const encoder = new TextEncoder();
const stream = new ReadableStream({ async start(controller) { // Send initial connection message controller.enqueue( encoder.encode(data: ${JSON.stringify({ type: 'connected' })}\n\n) );
// Subscribe to updates (e.g., from Redis) const subscription = subscribeToUpdates((event) => { controller.enqueue( encoder.encode(data: ${JSON.stringify(event)}\n\n) ); });
// Handle client disconnect request.signal.addEventListener('abort', () => { subscription.unsubscribe(); controller.close(); });
// Heartbeat to keep connection alive const heartbeat = setInterval(() => { controller.enqueue(encoder.encode(: heartbeat\n\n)); }, 30000);
request.signal.addEventListener('abort', () => { clearInterval(heartbeat); }); }, });
return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }); }
// Client: React hook export function useSSE(url: string, onMessage: (data: unknown) => void) { useEffect(() => { const eventSource = new EventSource(url);
eventSource.onmessage = (event) => { const data = JSON.parse(event.data); onMessage(data); };
eventSource.onerror = () => { // EventSource automatically reconnects console.log('SSE connection error, reconnecting...'); };
return () => eventSource.close(); }, [url, onMessage]); }
Presence System
Description
Track online/offline status
Example
// Server: Presence tracking with Redis import Redis from 'ioredis';
const redis = new Redis(); const PRESENCE_KEY = 'presence:online'; const PRESENCE_TTL = 60; // seconds
// User connects async function userOnline(userId: string) { await redis.zadd(PRESENCE_KEY, Date.now(), userId);
// Publish presence change await redis.publish('presence', JSON.stringify({ userId, status: 'online', timestamp: Date.now(), })); }
// Heartbeat (call every 30s) async function heartbeat(userId: string) { await redis.zadd(PRESENCE_KEY, Date.now(), userId); }
// User disconnects async function userOffline(userId: string) { await redis.zrem(PRESENCE_KEY, userId);
await redis.publish('presence', JSON.stringify({ userId, status: 'offline', timestamp: Date.now(), })); }
// Get online users async function getOnlineUsers(): Promise<string[]> { const cutoff = Date.now() - (PRESENCE_TTL * 1000); // Remove stale entries await redis.zremrangebyscore(PRESENCE_KEY, 0, cutoff); // Get current online users return redis.zrange(PRESENCE_KEY, 0, -1); }
// Check if user is online async function isUserOnline(userId: string): Promise<boolean> { const score = await redis.zscore(PRESENCE_KEY, userId); if (!score) return false; return parseInt(score) > Date.now() - (PRESENCE_TTL * 1000); }
// Client: Show presence function UserList({ users }) { const [onlineUsers, setOnlineUsers] = useState<Set<string>>(new Set());
useSSE('/api/presence', (event) => { if (event.status === 'online') { setOnlineUsers(prev => new Set([...prev, event.userId])); } else { setOnlineUsers(prev => { const next = new Set(prev); next.delete(event.userId); return next; }); } });
return ( <ul> {users.map(user => ( <li key={user.id}> <span className={onlineUsers.has(user.id) ? 'online' : 'offline'} /> {user.name} </li> ))} </ul> ); }
Typing Indicator
Description
Show when users are typing
Example
// Server: Typing indicator with debounce const typingUsers = new Map<string, NodeJS.Timeout>();
function handleTypingStart(roomId: string, userId: string) { // Clear existing timeout const existing = typingUsers.get(${roomId}:${userId}); if (existing) clearTimeout(existing);
// Broadcast typing start broadcastToRoom(roomId, { type: 'typing_start', userId, });
// Auto-stop after 3 seconds of no activity const timeout = setTimeout(() => { handleTypingStop(roomId, userId); }, 3000);
typingUsers.set(${roomId}:${userId}, timeout); }
function handleTypingStop(roomId: string, userId: string) { const timeout = typingUsers.get(${roomId}:${userId}); if (timeout) { clearTimeout(timeout); typingUsers.delete(${roomId}:${userId}); }
broadcastToRoom(roomId, { type: 'typing_stop', userId, }); }
// Client: Typing indicator component function TypingIndicator({ roomId }) { const [typingUsers, setTypingUsers] = useState<string[]>([]); const { send } = useWebSocket();
// Handle incoming typing events useEffect(() => { // Subscribed via WebSocket... }, []);
// Debounced typing notification const inputRef = useRef<HTMLInputElement>(null); const lastTypingRef = useRef(0);
const handleInput = () => { const now = Date.now(); if (now - lastTypingRef.current > 1000) { send({ type: 'typing_start', roomId }); lastTypingRef.current = now; } };
const handleBlur = () => { send({ type: 'typing_stop', roomId }); };
if (typingUsers.length === 0) return null;
return ( <div className="typing-indicator"> {typingUsers.length === 1 ? ${typingUsers[0]} is typing... : typingUsers.length === 2 ? ${typingUsers[0]} and ${typingUsers[1]} are typing... : ${typingUsers.length} people are typing...} </div> ); }
Scaling With Redis
Description
Scale WebSockets with Redis pub/sub
Example
// Each server subscribes to Redis channels import { WebSocketServer } from 'ws'; import Redis from 'ioredis';
const pub = new Redis(); const sub = new Redis();
// Local clients on this server const localClients = new Map<string, WebSocket>();
// Subscribe to Redis for cross-server messages sub.subscribe('broadcast', 'user-messages');
sub.on('message', (channel, message) => { const data = JSON.parse(message);
if (channel === 'broadcast') { // Send to all local clients for (const [, ws] of localClients) { if (ws.readyState === WebSocket.OPEN) { ws.send(message); } } } else if (channel === 'user-messages') { // Send to specific user if they're on this server const client = localClients.get(data.userId); if (client?.readyState === WebSocket.OPEN) { client.send(JSON.stringify(data.payload)); } } });
// When a message needs to go to all servers function broadcast(message: object) { pub.publish('broadcast', JSON.stringify(message)); }
// When a message needs to go to a specific user function sendToUser(userId: string, payload: object) { pub.publish('user-messages', JSON.stringify({ userId, payload })); }
// Connection handling wss.on('connection', (ws, req) => { const userId = authenticate(req); localClients.set(userId, ws);
ws.on('close', () => { localClients.delete(userId); }); });
Anti-Patterns
No Reconnection
Description
Not handling connection drops
Wrong
new WebSocket(url) with no reconnection logic
Right
Implement exponential backoff reconnection
Shared State
Description
Storing connections in memory with multiple servers
Wrong
const clients = new Map() with load balancer
Right
Use Redis pub/sub for cross-server communication
Sync In Handler
Description
Blocking operations in message handler
Wrong
await heavyDatabaseQuery() in onmessage
Right
Queue work, respond immediately if possible
Auth After Connect
Description
Authenticating after WebSocket is established
Wrong
ws.onopen = () => ws.send({ token })
Right
Pass token in connection URL or headers
Websockets Realtime - Sharp Edges
No Reconnection
Id
no-reconnection
Summary
Client doesn't handle disconnections
Severity
critical
Situation
User loses WiFi for 5 seconds. WebSocket closes. Page shows "disconnected" forever. User has to refresh. Happens constantly on mobile. Users complain about "unreliable" notifications.
Why
Networks are unreliable. Connections drop for many reasons: WiFi switches, mobile network changes, server restarts, load balancer timeouts. Without reconnection logic, any drop is permanent.
Solution
IMPLEMENT ROBUST RECONNECTION
class ReconnectingWebSocket { private ws: WebSocket | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 10; private baseDelay = 1000;
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.reconnectAttempts = 0; // Reset on success this.onConnect(); };
this.ws.onclose = (event) => { // 1000 = normal close, don't reconnect if (event.code === 1000) return;
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), 30000 // Max 30 seconds ); const jitter = delay 0.2 * Math.random();
setTimeout(() => { this.reconnectAttempts++; this.connect(); }, delay + jitter); } }
// Also handle page visibility document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { // Reconnect when user returns to tab if (!ws || ws.readyState !== WebSocket.OPEN) { connect(); } } });
// And online/offline events window.addEventListener('online', () => { connect(); });
Symptoms
- Users must refresh to reconnect
- "Unreliable" on mobile
- Silent failures
Detection Pattern
new WebSocket\\([^)]+\\)(?!.*reconnect)
No Heartbeat
Id
no-heartbeat
Summary
Connections timeout silently
Severity
high
Situation
Users stop receiving updates after being idle. No disconnect event fires. Server thinks they're still connected. Only discovered when user tries to send a message and it fails.
Why
Many proxies, load balancers, and NATs close idle connections after 30-60 seconds. Without heartbeat/ping, you don't know the connection is dead until you try to use it. Server-side memory leaks from "connected" dead clients.
Solution
IMPLEMENT HEARTBEAT
Server-side ping (preferred)
// ws library does this automatically const wss = new WebSocketServer({ server });
// But verify clients respond const clients = new Map();
function heartbeat() { this.isAlive = true; }
wss.on('connection', (ws) => { ws.isAlive = true; ws.on('pong', heartbeat); // Client responds to ping });
// Check every 30 seconds setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) { // Client didn't respond to last ping return ws.terminate(); }
ws.isAlive = false; ws.ping(); // Send ping, expect pong }); }, 30000);
Client-side heartbeat (for SSE or custom protocols)
function setupHeartbeat(ws) { let lastPong = Date.now();
// Send ping every 25 seconds const pingInterval = setInterval(() => { if (Date.now() - lastPong > 60000) { // No pong for 60s, connection dead ws.close(); clearInterval(pingInterval); return; }
ws.send(JSON.stringify({ type: 'ping' })); }, 25000);
ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'pong') { lastPong = Date.now(); } };
ws.onclose = () => clearInterval(pingInterval); }
Symptoms
- Silent connection drops
- Updates stop after idle period
- Server memory grows (dead connections)
Detection Pattern
Memory Leak Connections
Id
memory-leak-connections
Summary
Not cleaning up closed connections
Severity
high
Situation
Server memory usage grows over time. Eventually crashes or slows down. Clients in connection map never removed. Timers for disconnected clients keep running.
Why
Every WebSocket connection has associated state: client object, timers, subscriptions, event listeners. If not cleaned up on disconnect, memory accumulates. With thousands of connections, this adds up fast.
Solution
CLEAN UP EVERYTHING ON DISCONNECT
wss.on('connection', (ws, req) => { const userId = authenticate(req); const subscriptions: Subscription[] = []; const timers: NodeJS.Timeout[] = [];
// Track client clients.set(userId, ws);
// Setup subscriptions const sub = redis.subscribe(user:${userId}); subscriptions.push(sub);
// Setup timers const heartbeat = setInterval(() => { ws.ping(); }, 30000); timers.push(heartbeat);
// CRITICAL: Clean up on close ws.on('close', () => { // Remove from clients clients.delete(userId);
// Unsubscribe from all subscriptions.forEach(sub => sub.unsubscribe());
// Clear all timers timers.forEach(timer => clearInterval(timer));
// Update presence redis.zrem('online', userId);
console.log(Cleaned up ${userId}); });
// Also handle errors ws.on('error', (err) => { console.error(WebSocket error for ${userId}:, err); ws.close(); // Will trigger close handler }); });
// Monitor for leaks setInterval(() => { console.log(Active connections: ${wss.clients.size}); console.log(Memory: ${process.memoryUsage().heapUsed / 1024 / 1024}MB); }, 60000);
Symptoms
- Server memory grows over time
- "Ghost" clients in maps
- Server crashes after hours/days
Detection Pattern
Scaling Without Pubsub
Id
scaling-without-pubsub
Summary
Using in-memory state with multiple servers
Severity
high
Situation
Works on one server. Deploy second server behind load balancer. User A connects to server 1, user B to server 2. User A sends message to user B. Nothing happens. Users randomly can/can't communicate.
Why
In-memory Maps only exist on one server. When you scale horizontally, each server has its own clients Map. A message broadcast on server 1 never reaches clients on server 2.
Solution
USE REDIS PUB/SUB FOR CROSS-SERVER
import Redis from 'ioredis';
// Each server has its own pub/sub connections const pub = new Redis(process.env.REDIS_URL); const sub = new Redis(process.env.REDIS_URL);
// Local clients on THIS server only const localClients = new Map<string, WebSocket>();
// Subscribe to channels sub.subscribe('room:', 'user:');
sub.on('message', (channel, message) => { if (channel.startsWith('room:')) { // Broadcast to room members on this server const roomId = channel.split(':')[1]; broadcastToLocalRoom(roomId, message); } else if (channel.startsWith('user:')) { // Send to specific user if on this server const userId = channel.split(':')[1]; const client = localClients.get(userId); client?.send(message); } });
// When you want to broadcast function sendToRoom(roomId: string, message: object) { // Publishes to ALL servers pub.publish(room:${roomId}, JSON.stringify(message)); }
function sendToUser(userId: string, message: object) { // User might be on any server pub.publish(user:${userId}, JSON.stringify(message)); }
// Also use sticky sessions or store user -> server mapping // So you know which server to route to
Symptoms
- Works on dev, fails on prod
- Random users can't communicate
- Messages sometimes work
Detection Pattern
Auth After Upgrade
Id
auth-after-upgrade
Summary
Authenticating after WebSocket is established
Severity
high
Situation
WebSocket connects, then sends auth token as first message. Attacker connects thousands of unauthenticated WebSockets. Server runs out of memory/connections before validating any.
Why
Once WebSocket upgrade completes, server resources are allocated. If you wait for auth message, anyone can consume resources. You can't rate limit by user before knowing who they are.
Solution
AUTHENTICATE BEFORE UPGRADE
Option 1: Token in URL (common but visible in logs)
// Client const token = await getAuthToken(); const ws = new WebSocket(wss://api.example.com/ws?token=${token});
// Server 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 (e) { ws.close(4001, 'Unauthorized'); } });
Option 2: Cookie (automatic, secure)
// Client (browser sends cookies automatically) const ws = new WebSocket('wss://api.example.com/ws');
// Server wss.on('connection', (ws, req) => { const cookies = parseCookies(req.headers.cookie); const sessionId = cookies['session'];
const user = await getSessionUser(sessionId); if (!user) { ws.close(4001, 'Unauthorized'); return; }
ws.userId = user.id; });
Option 3: Ticket system (most secure)
// 1. Client gets ticket from auth endpoint const { ticket } = await fetch('/api/ws-ticket', { headers: { Authorization: Bearer ${token} } }).then(r => r.json());
// 2. Ticket is one-time use, short-lived await redis.setex(ws-ticket:${ticket}, 30, userId);
// 3. Connect with ticket const ws = new WebSocket(wss://api.example.com/ws?ticket=${ticket});
// 4. Server validates and consumes ticket wss.on('connection', async (ws, req) => { const ticket = new URL(req.url).searchParams.get('ticket'); const userId = await redis.get(ws-ticket:${ticket});
if (!userId) { ws.close(4001, 'Invalid ticket'); return; }
// Consume ticket (one-time use) await redis.del(ws-ticket:${ticket}); ws.userId = userId; });
Symptoms
- DoS vulnerability
- Resources exhausted by anon connections
- Can't rate limit properly
Detection Pattern
ws\\.send.token|ws\\.send.auth
Sse Over Http1
Id
sse-over-http1
Summary
SSE over HTTP/1.1 has connection limits
Severity
medium
Situation
User opens 6 browser tabs. 7th tab can't connect to SSE. Browser blocks because HTTP/1.1 limits 6 connections per domain. Real-time features stop working in some tabs.
Why
HTTP/1.1 browsers limit 6 persistent connections per origin. Each SSE connection uses one slot. With multiple tabs or components using SSE, you hit the limit fast. HTTP/2 multiplexes, but not all servers/proxies support it.
Solution
SOLUTIONS FOR SSE CONNECTION LIMITS
Option 1: Ensure HTTP/2 (recommended)
Nginx config
server { listen 443 ssl http2; # Enable HTTP/2
...
}
HTTP/2 multiplexes all requests over one connection
No 6-connection limit
Option 2: Single shared SSE connection
// Use SharedWorker or BroadcastChannel // One tab maintains SSE, broadcasts to others
// worker.js let eventSource = null; const ports = [];
self.onconnect = (e) => { const port = e.ports[0]; ports.push(port);
// Only first connection creates EventSource if (!eventSource) { eventSource = new EventSource('/events'); eventSource.onmessage = (event) => { ports.forEach(p => p.postMessage(event.data)); }; }
port.onmessage = () => { // Handle messages from tabs }; };
Option 3: Use WebSocket instead
// WebSocket doesn't have the same limit // Single connection, bidirectional // Better for multiple real-time features
Option 4: Domain sharding (hack)
// Distribute SSE across subdomains // sse1.example.com, sse2.example.com // 6 connections each = 12 total // But adds complexity and latency
Symptoms
- Some tabs don't receive updates
- Works with 1-2 tabs, fails with more
- HTTP/1.1 in production
Detection Pattern
new EventSource
Large Payload Broadcast
Id
large-payload-broadcast
Summary
Broadcasting large messages to many clients
Severity
medium
Situation
You broadcast a 100KB JSON object to 10,000 connected clients. Server freezes. Memory spikes to 1GB (100KB x 10000). Some clients timeout waiting. Others get disconnected.
Why
Serializing and sending large messages to many clients is O(n) in memory and CPU. Each send() buffers the message. With many clients, this overwhelms the server. Event loop is blocked during broadcast.
Solution
OPTIMIZE BROADCASTING
Chunk large broadcasts
async function broadcastLarge(clients, message) { const data = JSON.stringify(message); const BATCH_SIZE = 100;
for (let i = 0; i < clients.length; i += BATCH_SIZE) { const batch = clients.slice(i, i + BATCH_SIZE);
batch.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(data); } });
// Yield to event loop between batches await new Promise(resolve => setImmediate(resolve)); } }
Send references, not data
// Instead of sending full object ws.send(JSON.stringify({ type: 'update', data: hugeObject }));
// Send reference, let client fetch ws.send(JSON.stringify({ type: 'update', id: '123', fetchUrl: '/api/data/123' }));
Use compression
// Enable per-message deflate const wss = new WebSocketServer({ server, perMessageDeflate: { zlibDeflateOptions: { level: 6, // Compression level }, threshold: 1024, // Only compress > 1KB }, });
Debounce frequent updates
const pending = new Map();
function debouncedBroadcast(roomId, data) { const existing = pending.get(roomId); if (existing) { clearTimeout(existing.timer); existing.data = { ...existing.data, ...data }; } else { pending.set(roomId, { data, timer: null }); }
pending.get(roomId).timer = setTimeout(() => { actualBroadcast(roomId, pending.get(roomId).data); pending.delete(roomId); }, 100); // Batch updates within 100ms }
Symptoms
- Server freezes during broadcast
- Memory spikes
- Clients timeout or disconnect
Detection Pattern
Websockets Realtime - Validations
WebSocket without error handler
Id
no-error-handler
Severity
warning
Type
regex
Pattern
- new WebSocket\([^)]+\)(?![\s\S]*\.onerror)
- WebSocketServer\([^)]\)(?![\s\S]\.on\(["']error)
Message
WebSocket should have error handler
Fix Action
Add ws.onerror or ws.on('error') handler
Applies To
- *.js
- *.ts
- *.jsx
- *.tsx
WebSocket without close handler
Id
no-close-handler
Severity
warning
Type
regex
Pattern
- new WebSocket\\([^)]+\\)(?![\\s\\S]*\\.onclose)
Message
WebSocket should handle close events
Fix Action
Add ws.onclose handler with reconnection logic
Applies To
- *.js
- *.ts
- *.jsx
- *.tsx
Hardcoded WebSocket URL
Id
hardcoded-ws-url
Severity
info
Type
regex
Pattern
- new WebSocket\(["']ws://localhost
- new WebSocket\(["']wss://[a-z]+\.
Message
Consider using environment variable for WebSocket URL
Fix Action
Use process.env.NEXT_PUBLIC_WS_URL or similar
Applies To
- *.js
- *.ts
- *.jsx
- *.tsx
Insecure WebSocket (ws://)
Id
ws-without-wss
Severity
warning
Type
regex
Pattern
- new WebSocket\(['"]ws://
- WebSocket\([^)]*['"]ws://
Message
Use wss:// for secure WebSocket connections
Fix Action
Change ws:// to wss:// in production
Applies To
- *.js
- *.ts
- *.jsx
- *.tsx
Sending auth token in message
Id
auth-in-message
Severity
info
Type
regex
Pattern
- ws\\.send.*token
- ws\\.send.*auth
- socket\\.emit.*token
Message
Consider authenticating before connection instead of after
Fix Action
Pass token in URL or use ticket system
Applies To
- *.js
- *.ts
- *.jsx
- *.tsx
EventSource/WebSocket without cleanup
Id
event-listener-leak
Severity
warning
Type
regex
Pattern
- useEffect\\([^]new EventSource[^](?!return.*close)
- useEffect\\([^]new WebSocket[^](?!return.*close)
Message
Close connection in useEffect cleanup
Fix Action
Return cleanup function that calls close()
Applies To
- *.jsx
- *.tsx
setInterval without cleanup in WebSocket handler
Id
missing-interval-cleanup
Severity
warning
Type
regex
Pattern
- setInterval\\([^)]+\\)(?![\\s\\S]*clearInterval)
Message
Interval may not be cleaned up
Fix Action
Store interval ID and clear on disconnect
Applies To
- *.js
- *.ts
Synchronous broadcast to many clients
Id
sync-broadcast
Severity
info
Type
regex
Pattern
- forEach.*\\.send\\(
- for.of.\\.send\\(
Message
Consider chunking broadcasts for many clients
Fix Action
Use setImmediate between chunks to yield event loop
Applies To
- *.js
- *.ts
Sending large JSON without consideration
Id
large-json-send
Severity
info
Type
regex
Pattern
- \\.send\\(JSON\\.stringify\\([^)]{50,}
Message
Large payloads may cause performance issues
Fix Action
Consider pagination, compression, or sending references
Applies To
- *.js
- *.ts