Real-Time Communication
HardReal-time communication enables live updates in frontend applications without requiring the user to refresh. This covers WebSocket management, Server-Sent Events, long polling fallbacks, and the architectural patterns needed to handle presence, typing indicators, live feeds, and collaborative editing on the client side.
Interactive Visualization
Key Points
- WebSockets provide full-duplex communication for chat, collaboration, and gaming
- Server-Sent Events (SSE) are simpler than WebSockets for one-way server-to-client updates
- Long polling is a fallback when WebSocket/SSE connections are blocked by proxies
- Connection management must handle reconnection, backoff, and heartbeat/ping detection
- Presence systems track who is online using periodic heartbeats
- Operational Transform (OT) or CRDTs resolve conflicts in collaborative editing
- Message ordering requires sequence numbers or vector clocks for consistency
Code Examples
WebSocket Connection Manager
// Resilient WebSocket wrapper with auto-reconnect class WebSocketManager { private ws: WebSocket | null = null private reconnectAttempts = 0 private maxReconnectAttempts = 10 private listeners = new Map<string, Set<(data: unknown) => void>>() constructor(private url: string) {} connect(): void { this.ws = new WebSocket(this.url) this.ws.onopen = () => { this.reconnectAttempts = 0 this.emit('connected', null) } this.ws.onmessage = (event) => { const message = JSON.parse(event.data) this.emit(message.type, message.payload) } this.ws.onclose = (event) => { if (!event.wasClean) this.reconnect() } this.ws.onerror = () => this.reconnect() } private reconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) return const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000) this.reconnectAttempts++ setTimeout(() => this.connect(), delay) } subscribe(event: string, handler: (data: unknown) => void): () => void { if (!this.listeners.has(event)) this.listeners.set(event, new Set()) this.listeners.get(event)!.add(handler) return () => this.listeners.get(event)?.delete(handler) } private emit(event: string, data: unknown): void { this.listeners.get(event)?.forEach(handler => handler(data)) } send(type: string, payload: unknown): void { this.ws?.send(JSON.stringify({ type, payload })) } }
A production WebSocket manager handles reconnection with exponential backoff, event-based message routing, and clean subscription cleanup to prevent memory leaks.
Server-Sent Events for Live Feed
// SSE for one-way server-to-client updates function useLiveFeed(feedUrl: string) { const [events, setEvents] = useState<FeedEvent[]>([]) const [isConnected, setIsConnected] = useState(false) useEffect(() => { const source = new EventSource(feedUrl) source.onopen = () => setIsConnected(true) source.addEventListener('new-post', (e) => { const post = JSON.parse(e.data) as FeedEvent setEvents(prev => [post, ...prev].slice(0, 100)) // Cap at 100 }) source.addEventListener('update', (e) => { const update = JSON.parse(e.data) as FeedEvent setEvents(prev => prev.map(item => item.id === update.id ? update : item) ) }) source.onerror = () => { setIsConnected(false) // EventSource auto-reconnects by default } return () => source.close() }, [feedUrl]) return { events, isConnected } } // SSE advantages over WebSockets: // - Built-in reconnection // - Works over HTTP/2 // - Simpler server implementation // - Automatic event ID tracking for resuming
SSE is simpler than WebSockets for one-way data streams like feeds, notifications, and dashboards. The browser handles reconnection automatically, and events flow over standard HTTP.
Presence and Typing Indicators
// Presence system: who is online, who is typing interface PresenceState { onlineUsers: Map<string, { lastSeen: number }> typingUsers: Set<string> } function usePresence(ws: WebSocketManager, channelId: string) { const [presence, setPresence] = useState<PresenceState>({ onlineUsers: new Map(), typingUsers: new Set(), }) useEffect(() => { // Send heartbeat every 30 seconds const heartbeat = setInterval(() => { ws.send('presence:heartbeat', { channelId }) }, 30_000) // Listen for presence updates const unsubs = [ ws.subscribe('presence:join', (data) => { setPresence(prev => { const next = new Map(prev.onlineUsers) next.set((data as PresenceEvent).userId, { lastSeen: Date.now() }) return { ...prev, onlineUsers: next } }) }), ws.subscribe('presence:leave', (data) => { setPresence(prev => { const next = new Map(prev.onlineUsers) next.delete((data as PresenceEvent).userId) return { ...prev, onlineUsers: next } }) }), ws.subscribe('typing:start', (data) => { setPresence(prev => ({ ...prev, typingUsers: new Set(prev.typingUsers).add((data as PresenceEvent).userId), })) }), ws.subscribe('typing:stop', (data) => { setPresence(prev => { const next = new Set(prev.typingUsers) next.delete((data as PresenceEvent).userId) return { ...prev, typingUsers: next } }) }), ] return () => { clearInterval(heartbeat) unsubs.forEach(unsub => unsub()) } }, [ws, channelId]) return presence }
Presence tracking uses periodic heartbeats to detect online status. Typing indicators use start/stop events with automatic timeout to handle cases where the stop event is lost.
Common Mistakes
- Not implementing reconnection logic, leaving users in a broken state after network blips
- Opening a new WebSocket connection per component instead of sharing a single connection
- Forgetting to close WebSocket connections on component unmount, causing memory leaks
- Not handling message ordering, which leads to out-of-order updates in the UI
- Using WebSockets when SSE or polling would be simpler and sufficient
Interview Tips
- Explain the trade-offs between WebSocket, SSE, and polling for different use cases
- Discuss connection lifecycle: connect, authenticate, heartbeat, reconnect, disconnect
- Mention how you would handle offline/reconnection scenarios and message redelivery
- For collaborative editing, mention CRDTs vs Operational Transform at a high level
- Show awareness of scaling concerns: connection limits per server, fan-out cost