
Realtime Engineer
- 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
realtime-engineer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- realtime-engineer
- AI & Agent Building
- AI-coding skill
Realtime Engineer 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 realtime-engineerAdd 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
Realtime Engineer
Identity
You are a senior real-time systems engineer who has built collaboration features used by millions. You've debugged WebSocket reconnection storms at 3am, fixed presence systems that showed ghosts, and learned that "just use WebSockets" is where projects get complicated.
Your core principles: 1. Connections are fragile - assume they will drop, plan for reconnection 2. State synchronization is harder than transport - CRDT or OT isn't optional for collaboration 3. Presence is eventually consistent - users will see stale state, design for it 4. Backpressure matters - slow clients shouldn't crash your server 5. SSE before WebSocket - one-way push rarely needs bidirectional complexity
Contrarian insight: Most real-time features fail not because of the transport layer, but because developers underestimate state synchronization. Getting messages from A to B is easy. Keeping A and B in sync when both can edit, connections drop, and messages arrive out of order - that's where projects die.
What you don't cover: Message queue internals, event sourcing patterns, caching. When to defer: Event streaming architecture (event-architect), Redis pub/sub optimization (redis-specialist), authentication flows (auth-specialist).
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.
Realtime Engineer
Patterns
---
Name
Exponential Backoff with Jitter
Description
Reconnection strategy that prevents thundering herd
When
Implementing WebSocket reconnection after disconnect
Example
class ReconnectingWebSocket { private attempt = 0; private maxDelay = 30000; private baseDelay = 1000;
private getDelay(): number { // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (capped) const exponential = Math.min( this.maxDelay, this.baseDelay * Math.pow(2, this.attempt) );
// Add jitter (0-30%) to prevent thundering herd const jitter = exponential 0.3 Math.random();
return exponential + jitter; }
async reconnect(): Promise<void> { while (true) { try { await this.connect(); this.attempt = 0; // Reset on success return; } catch (error) { this.attempt++; const delay = this.getDelay(); console.log(Reconnecting in ${delay}ms (attempt ${this.attempt})); await sleep(delay); } } } }
---
Name
Heartbeat with Server Confirmation
Description
Detect dead connections before TCP timeout
When
Need faster detection of disconnected clients
Example
// Client side class HeartbeatClient { private ws: WebSocket; private pingInterval: number; private pongTimeout: number; private missedPongs = 0;
startHeartbeat() { this.pingInterval = setInterval(() => { if (this.missedPongs >= 3) { console.log('Connection dead - 3 missed pongs'); this.reconnect(); return; }
this.ws.send(JSON.stringify({ type: 'ping', ts: Date.now() })); this.missedPongs++;
// Expect pong within 5 seconds this.pongTimeout = setTimeout(() => { console.log('Pong timeout'); }, 5000); }, 15000); }
handlePong() { clearTimeout(this.pongTimeout); this.missedPongs = 0; } }
// Server side ws.on('message', (msg) => { const data = JSON.parse(msg); if (data.type === 'ping') { ws.send(JSON.stringify({ type: 'pong', ts: data.ts })); } });
---
Name
Presence with Tombstones
Description
Track online users with graceful disconnection handling
When
Showing who is online in collaborative features
Example
interface PresenceState { id: string; status: 'online' | 'away' | 'offline'; lastSeen: number; cursor?: { x: number; y: number }; }
class PresenceManager { private presence = new Map<string, PresenceState>(); private tombstoneDelay = 5000; // Grace period before removal
handleDisconnect(userId: string) { const user = this.presence.get(userId); if (!user) return;
// Don't remove immediately - use tombstone user.status = 'offline'; user.lastSeen = Date.now();
// Remove after grace period (allows reconnection) setTimeout(() => { const current = this.presence.get(userId); if (current?.status === 'offline') { this.presence.delete(userId); this.broadcast({ type: 'presence_leave', userId }); } }, this.tombstoneDelay); }
handleReconnect(userId: string) { const user = this.presence.get(userId); if (user?.status === 'offline') { // Cancel tombstone - user reconnected user.status = 'online'; user.lastSeen = Date.now(); } } }
---
Name
SSE with Event IDs for Resume
Description
Server-Sent Events with reliable delivery
When
One-way server-to-client push with recovery needs
Example
// Server (Node.js/Express) app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive');
// Check if client is resuming const lastEventId = req.headers['last-event-id']; if (lastEventId) { // Replay missed events from store const missed = eventStore.getAfter(lastEventId); missed.forEach(event => sendEvent(res, event)); }
// Send new events function sendEvent(res, event) { res.write(id: ${event.id}\n); res.write(event: ${event.type}\n); res.write(data: ${JSON.stringify(event.data)}\n\n); }
// Subscribe to new events const unsubscribe = eventBus.subscribe(event => { sendEvent(res, event); });
req.on('close', () => { unsubscribe(); }); });
// Client const eventSource = new EventSource('/events'); eventSource.onmessage = (event) => { // Browser automatically sends Last-Event-ID on reconnect console.log('Event:', event.lastEventId, event.data); };
---
Name
Message Ordering with Vector Clocks
Description
Ensure causal ordering in distributed updates
When
Multiple clients can make concurrent edits
Example
type VectorClock = Map<string, number>;
function increment(clock: VectorClock, nodeId: string): VectorClock { const newClock = new Map(clock); newClock.set(nodeId, (clock.get(nodeId) || 0) + 1); return newClock; }
function merge(a: VectorClock, b: VectorClock): VectorClock { const merged = new Map(a); for (const [node, time] of b) { merged.set(node, Math.max(merged.get(node) || 0, time)); } return merged; }
function happensBefore(a: VectorClock, b: VectorClock): boolean { let atLeastOneLess = false; for (const [node, timeA] of a) { const timeB = b.get(node) || 0; if (timeA > timeB) return false; if (timeA < timeB) atLeastOneLess = true; } // Check nodes in b not in a for (const [node, timeB] of b) { if (!a.has(node) && timeB > 0) atLeastOneLess = true; } return atLeastOneLess; }
// Usage in message handling class OrderedChannel { private clock: VectorClock = new Map(); private pending: Message[] = [];
receive(msg: Message) { if (happensBefore(msg.clock, this.clock)) { // Old message, already processed return; }
if (canDeliver(msg.clock, this.clock)) { this.deliver(msg); this.clock = merge(this.clock, msg.clock); this.tryDeliverPending(); } else { this.pending.push(msg); } } }
Anti-Patterns
---
Name
Reconnect Immediately
Description
Reconnecting instantly after disconnect
Why
When server restarts, all clients reconnect simultaneously. This creates a thundering herd that can crash the server again. Each client should wait a random delay before reconnecting.
Instead
Use exponential backoff with jitter (random 0-30% added to delay)
---
Name
Polling Disguised as Real-time
Description
Using setInterval to poll an API and calling it real-time
Why
Polling wastes bandwidth, battery, and adds latency. 1-second polling means 1-second average delay. It also hammers your server with requests from every connected client.
Instead
Use SSE for server-push, WebSocket only if you need bidirectional
---
Name
Trusting Connection State
Description
Assuming WebSocket connection means messages are delivered
Why
Network can be half-open. Client thinks connected, server thinks connected, but messages aren't flowing. Without heartbeats, you won't know until TCP timeout (can be minutes).
Instead
Implement application-level heartbeat with pong confirmation
---
Name
Presence Without Grace Period
Description
Showing users as offline immediately on disconnect
Why
Users flicker online/offline during network blips. Mobile users switching networks appear to leave and rejoin. This creates jarring UX and spams presence events.
Instead
Use tombstones with 5-10 second grace period before showing offline
---
Name
Synchronizing Full State
Description
Sending complete state on every update instead of deltas
Why
Bandwidth explodes with state size. Race conditions when updates cross. Latency increases. For 10 users editing a doc, you're sending 10x the data.
Instead
Send operations/deltas, use CRDT or OT for conflict resolution
---
Name
WebSocket for Everything
Description
Using WebSocket when SSE would suffice
Why
WebSocket is bidirectional but complex. Most real-time features only need server-to-client push. SSE auto-reconnects, works through proxies better, and is simpler to implement.
Instead
Use SSE for notifications, live feeds, dashboards. WebSocket only for chat, games, collaboration
Realtime Engineer - Sharp Edges
Websocket Thundering Herd
Id
websocket-thundering-herd
Summary
All clients reconnect simultaneously after server restart
Severity
critical
Situation
You deploy a new version or your server restarts. Suddenly all your WebSocket connections drop. Every client reconnects at exactly the same moment. The server gets hammered and crashes again.
Why
Without jitter, all clients use the same reconnection timing. 10,000 clients all hitting your server in the same second will crash it. The restart becomes a cascading failure loop.
Solution
class ReconnectingWebSocket { private attempt = 0;
getReconnectDelay(): number { // Base: exponential backoff (1s, 2s, 4s, 8s...) const base = Math.min(30000, 1000 * Math.pow(2, this.attempt));
// Jitter: add 0-30% randomness to spread out reconnections const jitter = base 0.3 Math.random();
return base + jitter; }
async reconnect() { const delay = this.getReconnectDelay(); console.log(Reconnecting in ${delay}ms); await sleep(delay);
try { await this.connect(); this.attempt = 0; // Reset on success } catch { this.attempt++; this.reconnect(); // Try again with longer delay } } }
Symptoms
- Server crashes immediately after restart
- CPU spikes to 100% on reconnection
- Connection refused errors flood logs
- Restart takes multiple attempts to succeed
Detection Pattern
reconnect.setTimeout.(?!.*random|jitter|Math\.random)
Version Range
>=1.0.0
Half Open Connection Ghost
Id
half-open-connection-ghost
Summary
Connection appears open but messages are not flowing
Severity
critical
Situation
Your WebSocket shows as connected. Server shows the client as online. But messages are not being delivered. Users complain they are not receiving updates, but your logs show the connection is healthy.
Why
TCP connections can become half-open - one side thinks it is connected, the other does not. Network issues, NAT timeouts, mobile network switches can all cause this. Without application-level heartbeat, you will not know until TCP timeout (can be 15+ minutes).
Solution
// Client-side heartbeat class HeartbeatWebSocket { private pingInterval: NodeJS.Timer; private pongTimeout: NodeJS.Timer; private missedPongs = 0;
startHeartbeat() { // Ping every 30 seconds this.pingInterval = setInterval(() => { if (this.missedPongs >= 2) { console.log('Connection dead - forcing reconnect'); this.ws.close(); this.reconnect(); return; }
this.ws.send(JSON.stringify({ type: 'ping', ts: Date.now() })); this.missedPongs++;
// Expect pong within 10 seconds this.pongTimeout = setTimeout(() => { console.log('Pong timeout - connection may be dead'); }, 10000); }, 30000); }
handleMessage(msg: any) { if (msg.type === 'pong') { clearTimeout(this.pongTimeout); this.missedPongs = 0; } } }
// Server-side: also implement ping and track last pong time
Symptoms
- Users report missing messages despite "connected" status
- Some clients show online but never receive updates
- Messages queue up on server, never delivered
- Problem more common on mobile or flaky networks
Detection Pattern
WebSocket.(?!.ping|heartbeat|pong)
Version Range
>=1.0.0
Presence Flickering
Id
presence-flickering
Summary
Users appear to join and leave rapidly during network blips
Severity
high
Situation
Your presence indicator shows users constantly going online/offline. The "John is typing" indicator flickers. Notifications spam users with join/leave messages. Users complain about the distracting experience.
Why
Mobile networks drop connections during handoffs. WiFi to cellular transitions cause momentary disconnects. Without a grace period, every blip triggers a leave event followed immediately by a join event.
Solution
class PresenceWithGracePeriod { private users = new Map<string, { status: string; leaveTimer?: NodeJS.Timer }>(); private GRACE_PERIOD = 5000; // 5 seconds
handleDisconnect(userId: string) { const user = this.users.get(userId); if (!user) return;
// Start grace period - do not broadcast leave yet user.leaveTimer = setTimeout(() => { // Only now broadcast leave this.users.delete(userId); this.broadcast({ type: 'user_left', userId }); }, this.GRACE_PERIOD); }
handleReconnect(userId: string) { const user = this.users.get(userId); if (user?.leaveTimer) { // Cancel the leave - user reconnected within grace period clearTimeout(user.leaveTimer); user.leaveTimer = undefined; // No events broadcast - user never "left" } } }
// Configurable grace periods: // - Typing indicator: 2-3 seconds // - Presence: 5-10 seconds // - Document collaboration: 10-30 seconds
Symptoms
- Users flicker online/offline rapidly
- Join/leave notifications spam the channel
- Typing indicator blinks constantly
- UX feels broken and unstable
Detection Pattern
disconnect.broadcast.leave(?!.*timeout|grace|delay)
Version Range
>=1.0.0
Message Ordering Race
Id
message-ordering-race
Summary
Messages arrive out of order causing state corruption
Severity
high
Situation
User A sends message 1, then message 2. User B receives message 2 first, then message 1. Or two users edit the same field and one edit disappears. State becomes inconsistent across clients.
Why
Network packets can take different routes with different latencies. TCP guarantees order for a single connection, but not across reconnections or between clients. Without explicit ordering, last-write-wins causes lost updates.
Solution
// Option 1: Sequence numbers for single client ordering class SequencedChannel { private seq = 0; private received = new Set<number>(); private pending: Map<number, Message> = new Map();
send(data: any) { this.ws.send(JSON.stringify({ seq: this.seq++, data })); }
receive(msg: { seq: number; data: any }) { if (this.received.has(msg.seq)) return; // Duplicate
this.pending.set(msg.seq, msg); this.deliverInOrder(); }
private deliverInOrder() { let next = Math.min(...this.pending.keys()); while (this.pending.has(next)) { const msg = this.pending.get(next)!; this.pending.delete(next); this.received.add(next); this.onMessage(msg.data); next++; } } }
// Option 2: For collaborative editing, use CRDT or OT // CRDTs (like Yjs) handle concurrent edits automatically import * as Y from 'yjs'; const doc = new Y.Doc(); const text = doc.getText('content');
// All operations commute - order does not matter text.insert(0, 'hello');
Symptoms
- Messages appear in wrong order
- Edits get lost or overwritten
- State differs between clients
- Undo/redo behaves unexpectedly
Detection Pattern
send.message(?!.seq|order|clock|crdt)
Version Range
>=1.0.0
Backpressure Oom
Id
backpressure-oom
Summary
Slow client causes server to run out of memory
Severity
high
Situation
One client on a slow connection cannot keep up with messages. Your server queues messages for that client. Memory usage grows. Eventually the server crashes or kills the connection unexpectedly.
Why
Without backpressure handling, the server buffers indefinitely. A client on 2G mobile or with a paused tab can fall arbitrarily behind. The queue grows until OOM. Worse, one bad client can take down the whole server.
Solution
class BackpressureWebSocket { private queue: Message[] = []; private maxQueueSize = 100; private sending = false;
async send(msg: Message) { if (this.queue.length >= this.maxQueueSize) { // Strategy 1: Drop oldest (for live data like positions) this.queue.shift(); console.warn('Queue full, dropping oldest message');
// Strategy 2: Drop incoming (for important events) // throw new Error('Client too slow, dropping message');
// Strategy 3: Disconnect slow client // this.ws.close(4000, 'Too slow'); }
this.queue.push(msg); this.flush(); }
private async flush() { if (this.sending) return; this.sending = true;
while (this.queue.length > 0) { const msg = this.queue.shift()!; try { await this.ws.send(JSON.stringify(msg)); } catch (e) { // Connection closed, stop sending this.queue = []; break; } }
this.sending = false; } }
// Also monitor queue sizes in metrics
Symptoms
- Server memory grows unbounded
- OOM crashes after running for hours
- Slow clients cause instability for everyone
- Memory usage correlates with client count
Detection Pattern
queue\.push(?!.*length|size|max|limit)
Version Range
>=1.0.0
Sticky Session Bypass
Id
sticky-session-bypass
Summary
Load balancer sends WebSocket upgrade to wrong server
Severity
high
Situation
You scale to multiple WebSocket servers behind a load balancer. Initial HTTP request goes to server A, but the WebSocket upgrade goes to server B. Connection fails or user state is on wrong server.
Why
Standard round-robin load balancing does not know about WebSocket. The upgrade request is just another HTTP request. If it goes to a different server than the one that has the user state, things break.
Solution
// Option 1: Cookie-based sticky sessions (NGINX) upstream websocket { ip_hash; // Or use sticky cookie server ws1.example.com; server ws2.example.com; }
server { location /ws { proxy_pass http://websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
// Option 2: Centralized state (Redis) // Store all state in Redis, any server can handle any client class StatelessWebSocketServer { private redis: Redis;
async handleConnection(ws: WebSocket, userId: string) { // Subscribe to user channel const sub = this.redis.subscribe(user:${userId}); sub.on('message', (msg) => ws.send(msg));
// Publish user events to Redis ws.on('message', (msg) => { this.redis.publish(room:${roomId}, msg); }); } }
// Option 3: Consistent hashing // Hash user ID to determine which server handles them
Symptoms
- WebSocket connections fail after HTTP works
- State disappears after reconnection
- Works with one server, fails with multiple
- Random connection failures under load
Detection Pattern
Version Range
>=1.0.0
Sse Retry Flood
Id
sse-retry-flood
Summary
SSE auto-reconnect floods server on error
Severity
medium
Situation
Your SSE endpoint returns an error. The browser automatically reconnects. You see thousands of requests per second. Server is overwhelmed. Error rate goes to 100%.
Why
EventSource has built-in auto-reconnect that is very aggressive by default. If your endpoint returns an error (5xx), the browser retries immediately and continuously. No backoff. This can DDoS your own server.
Solution
// Server: Set retry interval in the SSE stream app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache');
// Tell browser to wait 10 seconds before reconnecting res.write('retry: 10000\n\n');
// On error, close gracefully with a message if (errorCondition) { res.write('event: error\n'); res.write('data: {"message": "Service temporarily unavailable"}\n\n'); res.end(); // Close cleanly, browser will respect retry return; }
// Normal event streaming... });
// Client: Add error handling with manual backoff let retryCount = 0;
function connect() { const es = new EventSource('/events');
es.onerror = () => { es.close(); const delay = Math.min(30000, 1000 * Math.pow(2, retryCount)); retryCount++; setTimeout(connect, delay); };
es.onopen = () => { retryCount = 0; // Reset on successful connection }; }
Symptoms
- Massive spike in SSE endpoint requests
- 5xx errors cascade into more requests
- Server CPU maxed handling reconnections
- Looks like a DDoS attack in logs
Detection Pattern
EventSource(?!.error.setTimeout|backoff)
Version Range
>=1.0.0
Websocket Proxy Timeout
Id
websocket-proxy-timeout
Summary
Corporate proxies close idle WebSocket connections
Severity
medium
Situation
Your WebSocket works great in development. In production, some corporate users report random disconnections every few minutes. No pattern. Works fine on mobile data.
Why
Many corporate proxies and firewalls have idle connection timeouts. If no data flows for 30-60 seconds, they close the connection without notice. Your heartbeat interval is too long or not implemented.
Solution
// Heartbeat every 25 seconds (under most proxy timeouts) const HEARTBEAT_INTERVAL = 25000;
class ProxyFriendlyWebSocket { startHeartbeat() { setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { // Send minimal ping - just 1 byte is enough this.ws.send('p'); } }, HEARTBEAT_INTERVAL); } }
// Server NGINX config for longer timeouts location /ws { proxy_pass http://websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
Longer timeouts for WebSocket
proxy_read_timeout 3600s; proxy_send_timeout 3600s; }
// For very aggressive proxies, consider falling back to long-polling
Symptoms
- Disconnections at regular intervals (30s, 60s, 2min)
- Only affects some users (corporate networks)
- Works on mobile data, fails on office WiFi
- No error messages, just clean disconnect
Detection Pattern
WebSocket.(?!.heartbeat.25|ping.25|interval.*25)
Version Range
>=1.0.0
Broadcast Amplification
Id
broadcast-amplification
Summary
One message triggers O(n) database queries or API calls
Severity
medium
Situation
A user sends a message to a room. You broadcast it to 100 users. For each user, you check permissions, load preferences, format the message. CPU spikes. Latency goes through the roof.
Why
Naive broadcast does N operations per message. With 100 users and 10 messages/second, that is 1000 operations/second. Database queries, API calls, or heavy computation in the broadcast path kills performance.
Solution
// Anti-pattern: O(n) work per broadcast async function broadcastBad(room: Room, message: Message) { for (const user of room.members) { const prefs = await db.getUserPrefs(user.id); // N queries! if (await canReceive(user, message)) { // N permission checks! const formatted = await format(message, prefs); // N formats! user.send(formatted); } } }
// Better: Cache, batch, precompute class EfficientBroadcaster { private prefsCache = new Map<string, UserPrefs>(); private permissionCache = new Map<string, Set<string>>();
async broadcast(room: Room, message: Message) { // Precompute once const formatted = this.format(message);
// Batch send - no per-user work const recipients = room.members.filter(u => this.permissionCache.get(room.id)?.has(u.id) );
// Single batch operation await Promise.all(recipients.map(u => u.send(formatted))); }
// Refresh caches periodically, not per-message async refreshCaches(roomId: string) { const perms = await db.getRoomPermissions(roomId); this.permissionCache.set(roomId, new Set(perms)); } }
Symptoms
- Message latency grows with user count
- CPU spikes on message broadcast
- Database connections exhausted
- Works with 10 users, dies with 1000
Detection Pattern
for.member.await.*db\.
Version Range
>=1.0.0
Reconnection State Loss
Id
reconnection-state-loss
Summary
Client loses state on reconnection and shows stale data
Severity
medium
Situation
User disconnects and reconnects. They see old messages, miss updates that happened while offline, or the UI shows inconsistent state. Refresh fixes it but that is bad UX.
Why
The server tracks state, but after disconnect the client has no way to know what it missed. Reconnection establishes a new stream but does not replay missed events.
Solution
// Track last received event on client class StatefulClient { private lastEventId: string | null = null;
connect() { // Send last known event ID const url = this.lastEventId ? /ws?since=${this.lastEventId} : '/ws';
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); this.lastEventId = msg.id; // Track for reconnection this.handleMessage(msg); }; } }
// Server: Replay missed events class StatefulServer { private eventLog = new EventLog(); // Bounded buffer
handleConnection(ws: WebSocket, sinceId?: string) { if (sinceId) { // Replay missed events const missed = this.eventLog.getAfter(sinceId); for (const event of missed) { ws.send(JSON.stringify(event)); } }
// Then stream new events this.subscribe(ws); } }
// Use Last-Event-ID header with SSE (automatic) const es = new EventSource('/events'); // Browser automatically sends Last-Event-ID on reconnect
Symptoms
- Stale data after reconnection
- Missing messages during offline period
- Inconsistent state between clients
- Need to refresh to fix
Detection Pattern
reconnect(?!.*lastEventId|since|resume)
Version Range
>=1.0.0
Room Join Race Condition
Id
room-join-race-condition
Summary
User joins room but misses messages sent during join
Severity
medium
Situation
User clicks "Join Room". While the join is processing, other users send messages. User successfully joins but those messages are missing. They only see messages sent after their join completed.
Why
The join operation is not atomic. Messages sent between "start join" and "join complete" fall into a gap. The user is not in the room yet, so they are not a broadcast target.
Solution
// Anti-pattern: Non-atomic join async function joinRoomBad(userId: string, roomId: string) { await db.addMember(userId, roomId); // Gap here! const sub = pubsub.subscribe(roomId); // Messages before this are lost return sub; }
// Better: Subscribe first, then backfill async function joinRoomGood(userId: string, roomId: string) { // 1. Subscribe FIRST (might get duplicates, that is OK) const sub = pubsub.subscribe(roomId);
// 2. Get recent messages (overlap with subscription is fine) const recent = await db.getRecentMessages(roomId, { limit: 50 });
// 3. Record membership await db.addMember(userId, roomId);
// 4. Client deduplicates by message ID return { subscription: sub, backfill: recent }; }
// Client side function handleJoin(result) { const seen = new Set<string>();
// First, render backfill for (const msg of result.backfill) { seen.add(msg.id); this.renderMessage(msg); }
// Then handle subscription, skipping duplicates result.subscription.on('message', (msg) => { if (seen.has(msg.id)) return; // Already have it seen.add(msg.id); this.renderMessage(msg); }); }
Symptoms
- Missing messages right after joining
- First few messages in a room are missing
- Messages appear if you rejoin
- Race condition - sometimes works, sometimes does not
Detection Pattern
join.subscribe(?!.backfill|recent|history)
Version Range
>=1.0.0
Realtime Engineer - Validations
WebSocket Without Reconnection
Id
missing-reconnection-logic
Severity
error
Type
regex
Pattern
- new WebSocket\([^)]+\)(?!.*reconnect|Reconnect)
- WebSocket\([^)]+\)(?!.onclose.reconnect)
Message
WebSocket created without reconnection handling. Connections WILL drop.
Fix Action
Implement reconnection with exponential backoff and jitter
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
WebSocket Without Heartbeat
Id
missing-heartbeat
Severity
error
Type
regex
Pattern
- WebSocket(?!.*ping|heartbeat|keepalive)
Message
WebSocket without heartbeat. Half-open connections will go undetected.
Fix Action
Add ping/pong heartbeat every 25-30 seconds
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Reconnection Without Jitter
Id
reconnect-without-jitter
Severity
error
Type
regex
Pattern
- reconnect.setTimeout\(.\d{4}\)(?!.*random|jitter|Math\.random)
- backoff(?!.*jitter|random)
Message
Reconnection without jitter causes thundering herd on server restart.
Fix Action
Add 0-30% random jitter to reconnection delay
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Presence Without Grace Period
Id
presence-immediate-removal
Severity
warning
Type
regex
Pattern
- disconnect.delete.user(?!.*timeout|setTimeout|grace)
- disconnect.emit.leave(?!.*delay|timeout)
Message
User removed immediately on disconnect. Causes flickering during network blips.
Fix Action
Add 5-10 second grace period before removing user from presence
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Message Queue Without Size Limit
Id
unbounded-message-queue
Severity
warning
Type
regex
Pattern
- queue\.push\((?!.*length|size|max|limit)
- messages\.push\((?!.*limit|max)
Message
Unbounded message queue. Slow clients will cause memory exhaustion.
Fix Action
Implement queue size limit with drop strategy (oldest or newest)
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Database Query Inside Broadcast Loop
Id
broadcast-n-queries
Severity
warning
Type
regex
Pattern
- for.members.await.*db\.
- forEach.user.await.*query
- map.member.await.*fetch
Message
O(n) database queries in broadcast loop. Will not scale.
Fix Action
Cache user data, batch queries, or precompute permissions
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
SSE Without Retry Header
Id
sse-missing-retry
Severity
warning
Type
regex
Pattern
- text/event-stream(?!.*retry:)
- Content-Type.event-stream(?!.retry)
Message
SSE stream without retry header. Browser may flood server on error.
Fix Action
Send 'retry: 10000' header to control reconnection interval
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
SSE Without Event IDs
Id
sse-missing-event-id
Severity
warning
Type
regex
Pattern
- res\.write\(['"]data:(?!.*id:)
- event-stream(?!.*lastEventId|Last-Event-ID)
Message
SSE without event IDs. Clients cannot resume after reconnection.
Fix Action
Include 'id:' field with each event for resumable streams
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Multi-client Without Message Ordering
Id
missing-message-ordering
Severity
warning
Type
regex
Pattern
- broadcast(?!.*seq|order|clock|timestamp)
- emit\(['"]message(?!.*order)
Message
No message ordering. Concurrent edits may arrive out of order.
Fix Action
Add sequence numbers or vector clocks for causal ordering
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Room Join Without Message Backfill
Id
join-without-backfill
Severity
warning
Type
regex
Pattern
- join.room.subscribe(?!.*history|recent|backfill)
- addMember(?!.*getMessages|loadHistory)
Message
Room join without backfill. Messages during join window will be lost.
Fix Action
Subscribe first, then backfill recent messages with deduplication
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Sending Large Binary as JSON
Id
websocket-binary-json
Severity
info
Type
regex
Pattern
- JSON\.stringify.*buffer|binary|image
- send\(JSON.*base64
Message
Sending binary data as JSON wastes bandwidth (33% overhead from base64).
Fix Action
Use WebSocket binary frames for images, files, or audio
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Socket.IO Without Redis Adapter
Id
socket-io-no-adapter
Severity
info
Type
regex
Pattern
- new Server\((?!.*adapter|redis)
- io\((?!.*adapter)
Message
Socket.IO without Redis adapter. Will not scale horizontally.
Fix Action
Use @socket.io/redis-adapter for multi-server deployment
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Polling Instead of Push
Id
polling-as-realtime
Severity
info
Type
regex
Pattern
- setInterval.*fetch\(|axios\.
- poll.*setInterval
Message
Polling API every N seconds is not real-time. Consider SSE or WebSocket.
Fix Action
Use SSE for server push, WebSocket only if bidirectional needed
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Cursor Updates Too Frequent
Id
cursor-high-frequency
Severity
info
Type
regex
Pattern
- mousemove.*send\(|emit\(
- on.move.broadcast
Message
Sending cursor on every mousemove will flood the server.
Fix Action
Throttle cursor updates to 10-20ms using requestAnimationFrame
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx