Skip to content

WebSockets & Real-Time Communication

Traditional HTTP follows a strict request-response pattern: the client asks, the server answers, and the connection is done. But many modern applications need data to flow from the server to the client without the client asking for it first — chat messages, live notifications, stock tickers, collaborative editing, multiplayer games, and real-time dashboards all require some form of real-time communication.

This page covers the major techniques for achieving real-time communication on the web, from simple polling to full-duplex WebSockets, and helps you choose the right approach for your use case.


Techniques Overview

There are four primary approaches to real-time communication, each with distinct characteristics and trade-offs.

Short Polling

The simplest approach: the client repeatedly sends HTTP requests at a fixed interval to check for new data.

Client Server
│ │
│──── GET /messages?since=100 ────────►│
│◄─── 200 OK (no new messages) ───────│
│ │
│ ... wait 5 seconds ... │
│ │
│──── GET /messages?since=100 ────────►│
│◄─── 200 OK (no new messages) ───────│
│ │
│ ... wait 5 seconds ... │
│ │
│──── GET /messages?since=100 ────────►│
│◄─── 200 OK [{id: 101, text: "Hi"}] ─│
│ │

Pros: Simple to implement, works everywhere, stateless on the server Cons: Wasteful (many empty responses), high latency (up to one polling interval), high server load at scale

Long Polling

The client sends a request, and the server holds the connection open until new data is available or a timeout occurs.

Client Server
│ │
│──── GET /messages?since=100 ────────►│
│ │ (Server holds connection
│ ... server waits for data ... │ until data is available
│ │ or timeout)
│ │
│◄─── 200 OK [{id: 101, text: "Hi"}] ─│ New data arrived!
│ │
│──── GET /messages?since=101 ────────►│ Client immediately
│ │ reconnects
│ ... server waits again ... │
│ │

Pros: Lower latency than short polling, less wasted bandwidth, widely supported Cons: Still uses HTTP overhead per message, connection management complexity, not truly real-time

Server-Sent Events (SSE)

A one-way communication channel where the server pushes events to the client over a single, long-lived HTTP connection.

Client Server
│ │
│──── GET /events (Accept: text/ │
│ event-stream) ─────────────────►│
│ │
│◄─── 200 OK │
│ Content-Type: text/event-stream │
│ │
│◄─── data: {"user": "Alice"} ────────│ Server pushes event
│ │
│◄─── data: {"user": "Bob"} ──────────│ Server pushes event
│ │
│ ... connection stays open ... │
│ │
│◄─── data: {"user": "Charlie"} ──────│ Server pushes event
│ │

Pros: Simple, built on HTTP, automatic reconnection, event IDs for resume Cons: Unidirectional (server to client only), limited to text data, no binary support, limited browser connections per domain (6 in HTTP/1.1)

WebSockets

A full-duplex communication protocol that provides bidirectional data flow over a single, persistent TCP connection.

Client Server
│ │
│──── HTTP Upgrade Request ──────────►│
│◄─── 101 Switching Protocols ────────│
│ │
│═══════ WebSocket Connection ════════│
│ │
│──── "Hello from client" ───────────►│
│◄─── "Hello from server" ────────────│
│◄─── "New notification" ─────────────│
│──── "Typing..." ───────────────────►│
│◄─── "User joined" ─────────────────│
│ │
│ ... bidirectional at any time ...│
│ │
│──── Close frame ───────────────────►│
│◄─── Close frame ────────────────────│

Pros: Full-duplex, low latency, low overhead per message, binary and text support Cons: More complex to implement, requires WebSocket-aware infrastructure (load balancers, proxies), connection state management


Comprehensive Comparison Table

FeatureShort PollingLong PollingSSEWebSockets
DirectionClient to serverClient to serverServer to clientBidirectional
ProtocolHTTPHTTPHTTPWS (over TCP)
ConnectionNew connection per requestHeld open, reconnect after responseSingle persistent HTTPSingle persistent TCP
LatencyUp to polling intervalLow (near real-time)Low (near real-time)Very low (real-time)
Server overheadHigh (frequent requests)Medium (held connections)Low (single connection)Low (single connection)
Binary dataYes (via HTTP)Yes (via HTTP)No (text only)Yes
Auto-reconnectClient managesClient managesBuilt-in (EventSource API)Manual implementation
Browser supportUniversalUniversalAll modern browsersAll modern browsers
Through proxies/firewallsAlways worksUsually worksUsually worksMay need configuration
ScalabilityPoorModerateGoodGood
ComplexityVery lowLowLowMedium
Best forSimple dashboards, low-frequency updatesChat, notifications (simple)Live feeds, notifications, logsChat, gaming, collaboration, trading

WebSocket Protocol Deep Dive

The Upgrade Handshake

WebSocket connections begin as a standard HTTP request with an Upgrade header. This allows WebSockets to work through HTTP infrastructure (proxies, load balancers).

Client Request:
───────────────
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Origin: http://example.com
Server Response:
────────────────
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Key headers:

  • Upgrade: websocket — requests protocol switch from HTTP to WebSocket
  • Sec-WebSocket-Key — a random Base64-encoded value for security verification
  • Sec-WebSocket-Accept — the server’s proof that it received and accepted the key (computed as Base64(SHA1(key + GUID)))
  • Sec-WebSocket-Protocol — optional subprotocol negotiation

WebSocket Frames

After the handshake, data is exchanged as frames. Each frame has a small header (2-14 bytes) followed by the payload.

WebSocket Frame Structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len == 126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key, if MASK set to 1 (4 bytes) |
+---------------------------------------------------------------+
| Payload Data |
+---------------------------------------------------------------+
Frame types (opcodes):
0x0 = Continuation frame
0x1 = Text frame (UTF-8)
0x2 = Binary frame
0x8 = Close frame
0x9 = Ping frame
0xA = Pong frame

Key concepts:

  • FIN bit: Indicates the final fragment of a message (1 = final, 0 = more fragments coming)
  • Masking: Client-to-server frames must be masked (XOR with a 4-byte key) to prevent proxy cache poisoning
  • Ping/Pong: Heartbeat mechanism to keep the connection alive and detect disconnections

Connection Close

WebSocket connections are closed with a close handshake — either side can initiate.

Initiator Responder
│ │
│──── Close frame (code, reason) ─►│
│ │
│◄─── Close frame (code, reason) ──│
│ │
│ TCP connection closed │

Common close codes:

CodeMeaning
1000Normal closure
1001Going away (e.g., page navigation, server shutdown)
1002Protocol error
1003Unsupported data type
1006Abnormal closure (no close frame received)
1008Policy violation
1011Unexpected server error

Server-Sent Events (SSE) in Detail

SSE provides a standardized way for servers to push events to clients over HTTP. The client uses the EventSource API, which handles connection management, reconnection, and event parsing automatically.

SSE Message Format

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
id: 1
event: message
data: {"user": "Alice", "text": "Hello!"}
id: 2
event: message
data: {"user": "Bob", "text": "Hi Alice!"}
id: 3
event: user-joined
data: {"user": "Charlie"}
: this is a comment (ignored by the client)
id: 4
event: message
data: This is a multi-line message.
data: It spans two lines.
retry: 5000

Fields:

  • data: — the event payload (multiple data: lines are concatenated with newlines)
  • event: — the event type (default is “message”; custom types trigger specific event listeners)
  • id: — the event ID (sent as Last-Event-ID header on reconnection for resuming)
  • retry: — reconnection interval in milliseconds
  • Lines starting with : are comments (useful for keepalive)

SSE Features

FeatureDescription
Automatic reconnectionThe EventSource API reconnects automatically after disconnection
Event IDs and resumptionServer sends id: fields; on reconnect, client sends Last-Event-ID header
Custom event typesDifferent event types can trigger different handlers
Simple text protocolEasy to debug with curl or browser dev tools
HTTP compatibleWorks through existing HTTP infrastructure without configuration

When to Use Each Technique

ScenarioRecommended TechniqueWhy
Live sports scoresSSEServer-to-client updates, no client interaction needed
Chat applicationWebSocketsBidirectional messaging, low latency required
Notification feedSSE or Long PollingServer-to-client, moderate update frequency
Multiplayer gameWebSocketsBidirectional, very low latency, binary data
Stock tickerWebSockets or SSEHigh-frequency updates, server-to-client dominant
Collaborative document editingWebSocketsBidirectional, operational transforms or CRDTs
Dashboard with 30s refreshShort PollingSimple, low-frequency, tolerance for delay
Log streamingSSEServer-to-client, text data, auto-reconnect
IoT device commandsWebSocketsBidirectional communication with devices
File upload progressSSE or Short PollingServer-to-client progress updates

Decision framework:

  1. Do you need bidirectional communication? Use WebSockets
  2. Is it server-to-client only? Use SSE
  3. Can you tolerate seconds of delay? Use Long Polling or Short Polling
  4. Do you need binary data? Use WebSockets (SSE is text-only)
  5. Must it work through restrictive proxies? Start with Long Polling, fall back from WebSockets if needed

Code Examples

WebSocket Client and Server

# Server (using websockets library)
# pip install websockets
import asyncio
import websockets
import json
connected_clients = set()
async def handler(websocket):
# Register the client
connected_clients.add(websocket)
try:
async for message in websocket:
# Parse and broadcast to all clients
data = json.loads(message)
print(f"Received: {data}")
# Broadcast to all connected clients
broadcast_msg = json.dumps({
"user": data.get("user", "Anonymous"),
"text": data.get("text", ""),
"timestamp": "2025-01-15T10:30:00Z"
})
websockets.broadcast(connected_clients, broadcast_msg)
except websockets.ConnectionClosed:
print("Client disconnected")
finally:
connected_clients.discard(websocket)
async def main():
async with websockets.serve(handler, "localhost", 8765):
print("WebSocket server started on ws://localhost:8765")
await asyncio.Future() # Run forever
asyncio.run(main())
# Client
import asyncio
import websockets
import json
async def client():
async with websockets.connect("ws://localhost:8765") as ws:
# Send a message
await ws.send(json.dumps({
"user": "Alice",
"text": "Hello, everyone!"
}))
# Listen for messages
async for message in ws:
data = json.loads(message)
print(f"{data['user']}: {data['text']}")
asyncio.run(client())

Server-Sent Events (SSE)

# Server using Flask
# pip install flask
from flask import Flask, Response
import json
import time
app = Flask(__name__)
def event_stream():
"""Generator that yields SSE-formatted events."""
event_id = 0
while True:
event_id += 1
data = json.dumps({
"id": event_id,
"message": f"Server event #{event_id}",
"timestamp": time.time()
})
# SSE format: id, event type, and data fields
yield f"id: {event_id}\nevent: update\ndata: {data}\n\n"
time.sleep(2) # Send an event every 2 seconds
@app.route("/events")
def sse():
return Response(
event_stream(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no" # Disable nginx buffering
}
)
if __name__ == "__main__":
app.run(port=5000, threaded=True)

Production Considerations

WebSocket Scaling Challenges

ChallengeSolution
Sticky sessionsWebSocket connections are stateful; use load balancer sticky sessions or a pub/sub system (Redis, Kafka) to broadcast across servers
Connection limitsEach WebSocket connection consumes a file descriptor; tune OS limits (ulimit), use connection pooling
HeartbeatsImplement ping/pong frames to detect dead connections and free resources
ReconnectionClient must implement exponential backoff reconnection with jitter
AuthenticationAuthenticate during the HTTP upgrade handshake (cookies, tokens in query params, or first message)
Message orderingUse sequence numbers or timestamps to handle out-of-order delivery
Graceful shutdownSend close frames and drain connections before server restart

Reconnection with Exponential Backoff

Attempt 1: Wait 1 second + random jitter (0-500ms)
Attempt 2: Wait 2 seconds + random jitter (0-500ms)
Attempt 3: Wait 4 seconds + random jitter (0-500ms)
Attempt 4: Wait 8 seconds + random jitter (0-500ms)
Attempt 5: Wait 16 seconds + random jitter (0-500ms)
...
Maximum: Wait 60 seconds + random jitter (0-500ms)

Why jitter? Without jitter, if the server goes down and 10,000 clients try to reconnect, they all hit the server at the same exponential intervals (the “thundering herd” problem). Random jitter spreads reconnection attempts over time.


gRPC Streaming

gRPC natively supports four streaming patterns over HTTP/2, making it a powerful choice for real-time service-to-service communication.

PatternDescriptionUse Case
UnarySingle request, single responseStandard API call
Server streamingSingle request, stream of responsesLive feeds, log streaming
Client streamingStream of requests, single responseFile upload, sensor data
Bidirectional streamingBoth sides stream simultaneouslyChat, real-time sync
// gRPC service definition
syntax = "proto3";
service ChatService {
// Unary - single request/response
rpc GetRoom (GetRoomRequest) returns (Room);
// Server streaming - server sends stream of messages
rpc StreamMessages (StreamRequest) returns (stream ChatMessage);
// Client streaming - client sends stream of messages
rpc SendBulkMessages (stream ChatMessage) returns (Summary);
// Bidirectional streaming - both sides stream
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user = 1;
string text = 2;
int64 timestamp = 3;
}

When to use gRPC streaming over WebSockets:

  • Internal service-to-service communication (not browser-facing)
  • You need strong typing and code generation from .proto files
  • You want built-in flow control, cancellation, and deadline propagation
  • You are already using gRPC for unary calls and want to add streaming

Key Takeaways

  • Short polling is the simplest approach but wastes bandwidth and has high latency
  • Long polling reduces wasted requests but still has HTTP overhead per message
  • Server-Sent Events (SSE) are ideal for server-to-client streams with automatic reconnection and event IDs
  • WebSockets provide full-duplex, low-latency communication for bidirectional real-time features
  • Choose based on your requirements: direction of data flow, latency tolerance, binary data needs, and infrastructure constraints
  • In production, WebSockets require careful attention to connection management, heartbeats, reconnection, authentication, and horizontal scaling
  • gRPC streaming is the best choice for typed, real-time service-to-service communication

Next Steps