Real-time communication between client and server requires pushing data from server to client without the client repeatedly polling. Three main patterns exist: long polling (HTTP-based, simplest), Server-Sent Events (SSE, one-way streaming), and WebSockets (full-duplex). The choice depends on communication direction requirements, infrastructure constraints, and client capabilities.
Long Polling
The client sends an HTTP request; the server holds the connection open until an event occurs or a timeout is reached (typically 30-60 seconds). When the server responds, the client immediately sends a new request. This simulates push over standard HTTP without persistent connections. Drawbacks: each client ties up a server thread or file descriptor; 30-second timeouts generate high reconnect overhead; no multiplexing of multiple event streams on one connection.
Server-Sent Events (SSE)
SSE uses a persistent HTTP connection with Content-Type: text/event-stream. The server sends events as text lines: “data: {json}nn”. The browser EventSource API reconnects automatically with the Last-Event-ID header so the server can resume from where the client left off. SSE is one-directional (server to client only). It works through HTTP/2, supports multiplexing, and traverses proxies and firewalls more reliably than WebSockets. Best for dashboards, notifications, and live feeds where the client doesn't need to send data back.
WebSockets
WebSocket upgrades an HTTP connection to a persistent, full-duplex TCP channel. Both client and server can send messages at any time. The upgrade handshake uses HTTP Upgrade header. WebSocket frames have minimal overhead (2-14 bytes per frame vs full HTTP headers). Best for interactive applications: chat, collaborative editing, online games, trading terminals, where bidirectional low-latency communication is required.
WebSocket Connection Lifecycle
Client opens connection: GET /ws HTTP/1.1 with Upgrade: websocket, Sec-WebSocket-Key. Server responds 101 Switching Protocols. Both sides exchange frames. Connection terminates with a close frame (code + reason). Implement heartbeat (ping/pong frames every 30s) to detect dead connections through NAT and load balancers that timeout idle TCP connections. Reconnect with exponential backoff on disconnect.
Scaling WebSocket Servers
WebSocket connections are long-lived; a single server handles tens of thousands of concurrent connections using non-blocking I/O (Node.js, Netty, Go goroutines). Load balancers must support sticky sessions (route reconnecting clients to the same server) or use a shared pub/sub backend (Redis Pub/Sub, Kafka) so any server can receive messages intended for connections held by other servers. A message broker decouples senders from the specific server holding each connection.
Presence and Connection State
Track which users are online by maintaining a connection registry: on connect, write {user_id, server_id, connected_at} to Redis. On disconnect, delete the entry. Set a TTL on the entry and refresh on each heartbeat to handle abrupt disconnections. Query the registry to determine if a user is online before sending a push notification (no point pushing if user is actively connected to WebSocket). The registry also enables fanout: find all connections for a user and deliver to each.
Choosing the Right Pattern
Use long polling when: you can't change server infrastructure, need compatibility with strict firewalls, have low message frequency (<1/minute). Use SSE when: communication is server-to-client only, you want simplicity and automatic reconnection, you need to pass through HTTP/2 proxies. Use WebSockets when: you need bidirectional communication, low latency (<100ms), high message frequency, or rich interactive features. Most modern real-time applications (chat, collaboration, gaming) use WebSockets.