Real-Time Communication

Hard

Real-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

Related Concepts