| name | implementing-websocket-realtime |
| description | Use this skill when implementing real-time features with WebSockets, including connection lifecycle management, pub/sub event patterns, state synchronization between client and server, event broadcasting, and integration with frontend state management (React Query, SWR, etc.). Covers FastAPI/Node.js/Express backends and React/Next.js/Vue frontends with fallback strategies and error recovery. |
WebSocket Real-Time Implementation
Decision Tree: Real-Time Protocol Selection
| Requirement | Protocol | Why |
|---|
| Bidirectional, instant updates | WebSocket | Full duplex, low latency |
| Server โ client only | SSE | Simpler, auto-reconnect |
| Infrequent updates (<1/min) | Polling | Simpler, no persistent connection |
| Mobile/unstable network | WS + Fallback | Graceful degradation |
Decision: Use WebSocket for real-time collaboration, SSE for notifications, polling as last resort.
Generic Event Structure
interface WebSocketEvent {
topic: string;
event: EventType;
data: {
entity_id: string;
payload: unknown;
user_id?: string;
tenant_id?: string;
timestamp: string;
};
trace_id?: string;
}
type EventType = "ADDED" | "UPDATED" | "DELETED" | "STATUS_CHANGED" | string;
Validation: See ./scripts/validate-ws-event.js
Connection Lifecycle
1. Initial Connection
const ws = new WebSocket(`${WS_URL}?token=${authToken}`);
ws.onopen = () => {
console.log('Connected');
subscribeToTopics(['gift-list:123', 'user:456']);
};
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, token: str):
await websocket.accept()
user = authenticate(token)
connection_manager.connect(websocket, user.id)
2. Authentication
Options:
- Query param:
?token=jwt (simplest)
- First message: Send auth message after connect
- Cookie: Use existing HTTP session
Recommendation: Query param for stateless, first message for flexible auth.
3. Subscription Management
function subscribe(topics: string[]) {
ws.send(JSON.stringify({
type: 'subscribe',
topics: ['gift-list:123', 'user:456']
}));
}
function unsubscribe(topics: string[]) {
ws.send(JSON.stringify({
type: 'unsubscribe',
topics: ['gift-list:123']
}));
}
4. Heartbeat/Keepalive
const heartbeat = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'pong') {
lastPong = Date.now();
}
};
5. Reconnection Logic
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const INITIAL_DELAY = 1000;
function reconnect() {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error('Max reconnection attempts reached');
return;
}
const delay = INITIAL_DELAY * Math.pow(2, reconnectAttempts);
setTimeout(() => {
reconnectAttempts++;
connect();
}, delay);
}
ws.onclose = () => {
console.log('Connection closed, reconnecting...');
reconnect();
};
6. Cleanup
useEffect(() => {
return () => {
if (ws) {
ws.close();
clearInterval(heartbeat);
}
};
}, []);
Details: See ./connection-lifecycle.md
State Synchronization Pattern
Flow
1. Load initial data โ REST API (React Query)
2. Subscribe to updates โ WebSocket (on mount)
3. Receive event โ Invalidate cache โ React Query refetches
4. Optimistic update โ Update UI immediately, rollback on error
5. Unsubscribe โ WebSocket (on unmount)
6. Fallback โ Poll every 10s if WS fails
Implementation
const { data, isLoading } = useQuery({
queryKey: ['gift-list', listId],
queryFn: () => fetchGiftList(listId),
});
useEffect(() => {
const ws = connectWebSocket();
ws.onmessage = (event) => {
const wsEvent: WebSocketEvent = JSON.parse(event.data);
if (wsEvent.topic === `gift-list:${listId}`) {
queryClient.invalidateQueries(['gift-list', listId]);
}
};
ws.send(JSON.stringify({
type: 'subscribe',
topics: [`gift-list:${listId}`]
}));
return () => {
ws.send(JSON.stringify({
type: 'unsubscribe',
topics: [`gift-list:${listId}`]
}));
ws.close();
};
}, [listId]);
const mutation = useMutation({
mutationFn: updateGift,
onMutate: async (newGift) => {
await queryClient.cancelQueries(['gift-list', listId]);
const previous = queryClient.getQueryData(['gift-list', listId]);
queryClient.setQueryData(['gift-list', listId], (old) => ({
...old,
gifts: old.gifts.map(g => g.id === newGift.id ? newGift : g)
}));
return { previous };
},
onError: (err, newGift, context) => {
queryClient.setQueryData(['gift-list', listId], context.previous);
},
});
Details: See ./state-sync-strategies.md
Implementation Checklist
Backend Setup
Frontend Setup
Testing
Details: See ./backend-patterns.md and ./frontend-patterns.md
Backend Patterns
FastAPI Connection Manager
from fastapi import WebSocket
from typing import Dict, Set
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {}
self.subscriptions: Dict[str, Set[str]] = {}
async def connect(self, websocket: WebSocket, user_id: str):
self.active_connections[user_id] = websocket
def disconnect(self, user_id: str):
if user_id in self.active_connections:
del self.active_connections[user_id]
def subscribe(self, user_id: str, topic: str):
if topic not in self.subscriptions:
self.subscriptions[topic] = set()
self.subscriptions[topic].add(user_id)
async def broadcast(self, topic: str, event: dict):
if topic not in self.subscriptions:
return
for user_id in self.subscriptions[topic]:
if user_id in self.active_connections:
ws = self.active_connections[user_id]
await ws.send_json(event)
manager = ConnectionManager()
Full examples: See ./backend-patterns.md
Frontend Patterns
React Hook: useWebSocket
function useWebSocket(topics: string[]) {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
const queryClient = useQueryClient();
useEffect(() => {
const ws = new WebSocket(`${WS_URL}?token=${getToken()}`);
ws.onopen = () => {
setIsConnected(true);
ws.send(JSON.stringify({ type: 'subscribe', topics }));
};
ws.onmessage = (event) => {
const wsEvent: WebSocketEvent = JSON.parse(event.data);
queryClient.invalidateQueries([wsEvent.topic.split(':')[0]]);
};
ws.onclose = () => {
setIsConnected(false);
};
wsRef.current = ws;
return () => {
ws.send(JSON.stringify({ type: 'unsubscribe', topics }));
ws.close();
};
}, [topics.join(',')]);
return { isConnected };
}
Full examples: See ./frontend-patterns.md
Progressive Disclosure References
For detailed patterns and examples:
- Connection Lifecycle:
./connection-lifecycle.md
- Event Structure & Validation:
./event-structure-patterns.md
- State Sync Strategies:
./state-sync-strategies.md
- Backend Implementations:
./backend-patterns.md
- Frontend Implementations:
./frontend-patterns.md
- Fallback & Recovery:
./fallback-strategies.md
- Event Validator Script:
./scripts/validate-ws-event.js