Real-Time Protocols

WebSocket vs Server-Sent Events: Choosing the Right Protocol

Head-to-head comparison of WebSocket and SSE — bidirectional vs unidirectional, browser support, reconnection, and when each protocol shines.

Protocol Fundamentals

WebSocket and Server-Sent Events (SSE) both enable servers to push data to browsers without the client repeatedly polling. But they solve different problems and make very different trade-offs.

WebSocket: The Full-Duplex Channel

WebSocket starts life as an HTTP/1.1 request with a special Upgrade header:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After the 101 response, the TCP connection is repurposed as a bidirectional message channel. Either side can send frames at any time — text frames, binary frames, ping/pong frames (keep-alive), and close frames. There is no request/response pairing: the server can push unsolicited data, and the client can send messages without waiting for a server response.

WebSocket frames have a small overhead (2–14 bytes per message) compared to HTTP headers (hundreds of bytes). This makes it well-suited for high-frequency messaging where each individual payload is small.

SSE: The Unidirectional Stream

Server-Sent Events are a much simpler protocol. The client makes a standard HTTP GET request, and the server keeps the response stream open indefinitely, pushing newline-delimited events:

GET /events HTTP/1.1
Accept: text/event-stream

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

data: {"type": "price_update", "symbol": "BTC", "price": 95000}

event: heartbeat
data: ping

id: 42
data: {"type": "news", "headline": "Fed holds rates"}

The id field is the secret weapon of SSE: the browser stores the last event ID and sends it as Last-Event-ID on reconnection. The server can resume the stream from exactly where it left off. This built-in resumption is something you must implement manually with WebSocket.

SSE only flows server → client. If the client needs to send data, it uses a separate POST request. This asymmetry is a feature for read-heavy use cases — the server does not need to handle arbitrary incoming messages.

Feature Comparison

FeatureWebSocketSSE
DirectionBidirectionalServer → Client only
ProtocolCustom framing over TCPHTTP (stays HTTP)
Binary supportYes (binary frames)No (text only)
Auto-reconnectNo — must implementYes — browser built-in
Last event IDNo — must implementYes — `Last-Event-ID` header
HTTP/2 multiplexingNo — separate connectionYes — multiplexed over H2
Proxies/firewallsSometimes blockedRarely blocked (plain HTTP)
Message orderingYes (TCP)Yes (TCP)
Browser supportAll modern browsersAll modern browsers (no IE11 native)
Max connections/browserNo limit6 per origin (HTTP/1.1), unlimited (H2)

The HTTP/2 connection limit difference is significant. With HTTP/1.1, browsers cap SSE at 6 concurrent connections per origin — a problem for multi-tab applications. HTTP/2 multiplexes all SSE streams over a single TCP connection, eliminating this limit entirely. WebSocket connections are not subject to the HTTP/2 multiplexing benefit because they operate at the TCP layer.

Use Case Matrix

Choose WebSocket when:

Chat and messaging — users both send and receive messages. You need the bidirectional channel; SSE would require a separate POST endpoint for sending messages and add latency.

Collaborative editing (Google Docs style) — users send cursor positions, text operations, and presence signals while receiving the same from others. The operational transform or CRDT merge needs a true duplex channel.

Online gaming — game state updates (position, health, events) flow from server to client at 20–60 Hz, but player actions (movement, shooting) flow from client to server. Binary frames allow efficient encoding of game state structs.

Financial trading UIs — order entry requires client→server messages while market data streams server→client. The same WebSocket connection handles both.

Choose SSE when:

Live dashboards and analytics — server pushes aggregated metrics, counters, and chart data. The client never sends data back. SSE's simplicity means less server-side infrastructure.

Live news feeds and notifications — push breaking news, alerts, or activity feeds to users. The built-in reconnect handles mobile network transitions gracefully.

Progress streaming — a long-running server process (export, build, AI inference) streams progress updates. SSE + Last-Event-ID lets the client reconnect and continue watching without missing steps.

Log streaming and CI/CD — real-time log output from builds or deployments. Server pushes lines as they arrive. The client only reads.

Scalability Considerations

Connection Limits and Load Balancers

Both protocols maintain persistent connections — a WebSocket or SSE connection keeps an OS file descriptor open on the server for its entire lifetime. A Node.js or Go server can comfortably hold 10,000–100,000 concurrent connections. Python WSGI servers cannot: each connection blocks a thread. Use an async framework (FastAPI with uvicorn, Django Channels with Daphne) for either protocol.

Load balancers must be configured for sticky sessions (session affinity) with WebSocket, because the bidirectional state lives on a specific server process. SSE is slightly more flexible: since messages are server-initiated, you can use a pub/sub backend (Redis, Kafka) to fan out to SSE connections on any node.

# Django Channels — WebSocket consumer
from channels.generic.websocket import AsyncWebsocketConsumer
import json

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room = self.scope['url_route']['kwargs']['room_name']
        await self.channel_layer.group_add(self.room, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.room, self.channel_name)

    async def receive(self, text_data):
        data = json.loads(text_data)
        await self.channel_layer.group_send(
            self.room,
            {'type': 'chat_message', 'message': data['message']}
        )

    async def chat_message(self, event):
        await self.send(text_data=json.dumps({'message': event['message']}))
# Django SSE view — simpler, no channel layer needed for pub/sub
import asyncio
from django.http import StreamingHttpResponse

async def sse_dashboard(request):
    async def event_stream():
        last_id = int(request.headers.get('Last-Event-ID', 0))
        while True:
            metrics = await fetch_metrics(since_id=last_id)
            for m in metrics:
                last_id = m['id']
                yield f'id: {m["id"]}\ndata: {json.dumps(m)}\n\n'
            await asyncio.sleep(1)

    response = StreamingHttpResponse(event_stream(), content_type='text/event-stream')
    response['Cache-Control'] = 'no-cache'
    return response

Decision Framework

Work through this sequence:

  • Does the client send data in real time? If yes → WebSocket. If no (client only reads) → SSE is simpler.
  • Do you need binary message framing? Images, audio, compressed game state → WebSocket. JSON or text → SSE handles it fine.
  • Are you running behind an HTTP/2 gateway? If yes, SSE's connection limit problem disappears. If you are on HTTP/1.1 only and users will open many tabs → consider WebSocket to avoid the 6-connection cap.
  • How much infrastructure can you invest? SSE is a streaming HTTP response — no special server software needed beyond async support. WebSocket requires a protocol upgrade handler and typically a channel layer for horizontal scaling.
  • Do you need automatic reconnection with message replay? SSE gives you this for free. WebSocket requires you to implement exponential backoff reconnection and a server-side event log to replay missed events.

The most common mistake: choosing WebSocket because it sounds more powerful, then spending weeks building reconnection logic, session affinity, and message replay that SSE would have provided automatically. If your data only flows server → client, SSE is the right tool.

البروتوكولات ذات الصلة

مصطلحات ذات صلة

المزيد في Real-Time Protocols