tests.ws

WebSocket vs SSE - Which One to Use?

websocket sse server-sent-events real-time comparison

WebSocket provides full bidirectional communication over a single TCP connection, while Server-Sent Events (SSE) provides server-to-client only streaming over standard HTTP. Both solve the “server push” problem, but they make very different tradeoffs in complexity, compatibility, and capability. When choosing between SSE vs WebSocket or comparing SSE WebSocket options, understanding these tradeoffs is essential.

If your application only needs to receive updates from the server, SSE is often the simpler and better choice. If you need the client to send frequent messages back to the server over the same connection, WebSocket is the right tool.

Quick Comparison Table

FeatureSSEWebSocket
Data directionServer to client onlyBidirectional
ProtocolHTTP (standard)ws:// / wss:// (upgrade from HTTP)
Binary dataNo (text only)Yes (text and binary frames)
Auto-reconnectBuilt-inYou must implement it yourself
Max connections6 per domain (HTTP/1.1)No browser-imposed limit
Browser supportAll modern browsers (no IE)All modern browsers (including IE 10+)
Proxy/firewall supportExcellent (standard HTTP)Sometimes blocked
Implementation complexityLowMedium
HTTP/2 multiplexingYesNo
Message overhead~few bytes per event2-6 bytes per frame

How SSE Works

Server-Sent Events use the EventSource browser API. The client opens a standard HTTP connection, and the server holds it open, sending events as plain text in a specific format.

The server responds with Content-Type: text/event-stream and writes events to the response stream without closing it. Each event follows a simple line-based format:

data: This is a message

event: notification
data: {"type": "alert", "message": "Server restarted"}

id: 42
event: update
data: New data arrived
retry: 5000

Four fields control the behavior of each event:

  • data: contains the event payload. Multiple data: lines are joined with newlines.
  • event: sets the event type. The client can listen for specific types instead of the generic message event.
  • id: assigns an ID to the event. If the connection drops, the browser sends the last received ID in the Last-Event-ID header when reconnecting.
  • retry: tells the browser how many milliseconds to wait before attempting reconnection.

The automatic reconnection behavior is one of the strongest advantages SSE has over WebSocket. When the connection drops, the browser reconnects on its own and tells the server which event it last received. The server can then replay missed events. You get reliable delivery without writing any reconnection logic on the client.

How WebSocket Works

WebSocket starts as an HTTP request with an Upgrade header. If the server agrees, the connection switches from HTTP to the WebSocket protocol. From that point, both sides can send frames to each other at any time.

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

After the handshake completes, the HTTP connection is gone. Communication happens through WebSocket frames, which carry either text or binary data with minimal overhead (as little as 2 bytes per frame). For a full explanation of the protocol, read the WebSocket guide.

Detailed Comparison

Connection Model

SSE runs over plain HTTP. Your existing infrastructure, load balancers, CDNs, and reverse proxies all understand it without special configuration. The connection is a long-lived HTTP response, which fits naturally into the request/response model that every web server already supports.

WebSocket upgrades the HTTP connection to a different protocol entirely. This means your load balancer needs to support WebSocket connections. Your reverse proxy needs explicit configuration. Some corporate firewalls block WebSocket traffic because they do not recognize the protocol.

Data Direction

SSE is strictly one-way: the server sends events to the client. If the client needs to send data back, it makes a separate HTTP request (POST, PUT, etc.). This is perfectly fine for many real-time applications. Think about a stock ticker, a news feed, or a notification system. The client rarely needs to push data to the server through the same channel.

WebSocket is bidirectional. Either side can send a message at any time without waiting for the other. This matters when you need low-latency, high-frequency communication in both directions, like a multiplayer game or a collaborative editor.

Data Format

SSE transmits text only. You can send JSON strings, but you cannot send binary data directly. If you need to stream binary content (audio, images, protocol buffers), SSE is not the right choice.

WebSocket supports both text and binary frames. You can send ArrayBuffers, Blobs, or typed arrays alongside text messages. This makes it suitable for applications that need to transfer binary data efficiently.

Reconnection

SSE handles reconnection automatically. When the connection drops, the browser waits for the retry interval (default is usually 3 seconds) and reconnects. It includes the Last-Event-ID header so the server knows where the client left off. You write zero reconnection code on the client side.

WebSocket gives you nothing. When a WebSocket connection closes, you need to detect the closure, implement backoff logic, and reconnect manually. You also need to handle the case where messages were sent during the disconnection window. Libraries like Socket.IO exist largely because reconnection is such a common need.

HTTP/2 Compatibility

SSE benefits greatly from HTTP/2. Under HTTP/1.1, browsers limit you to 6 concurrent connections per domain. If you open 6 SSE connections, you have consumed all available connections for that domain, and regular page requests will queue. Under HTTP/2, all streams multiplex over a single TCP connection, so SSE connections do not count against any practical limit.

WebSocket does not multiplex over HTTP/2. Each WebSocket connection requires its own TCP connection, regardless of the HTTP version. The IETF has proposed RFC 8441 for bootstrapping WebSocket over HTTP/2, but browser support is still limited.

Proxy and Firewall Behavior

SSE passes through virtually every proxy and firewall without issues. It is a standard HTTP response with a specific content type. Firewalls see normal HTTP traffic.

WebSocket connections can be problematic. Some proxies do not understand the Upgrade mechanism. Some firewalls block non-HTTP traffic on port 80 or 443. In practice, using wss:// (WebSocket over TLS) resolves most of these issues because the encrypted connection prevents proxies from inspecting and interfering with the traffic.

Browser Connection Limits

Under HTTP/1.1, browsers enforce a limit of approximately 6 connections per domain. This limit applies to all HTTP connections, including SSE. If you open 3 SSE connections, you only have 3 connections left for fetching images, scripts, and API calls. This is a real constraint in production.

WebSocket connections are not subject to this limit. You can open many WebSocket connections without affecting HTTP traffic. Under HTTP/2, the SSE connection limit problem goes away because all streams share one TCP connection.

Message Overhead

SSE messages carry a small amount of overhead from the text-based format. Each event includes field names (data:, id:, etc.) and newline delimiters. For most applications, this overhead is negligible.

WebSocket frames have 2 to 6 bytes of framing overhead for small messages (up to 125 bytes). Client-to-server frames add 4 bytes for the masking key. For high-frequency messaging, WebSocket is more efficient at the wire level.

SSE Code Example

Here is a working SSE implementation with a Node.js Express server and a browser client.

Server (Node.js + Express)

const express = require("express");
const app = express();

app.get("/events", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  // Send a comment line to prevent proxy buffering
  res.write(": connected\n\n");

  let eventId = 0;

  const interval = setInterval(() => {
    eventId++;
    const data = JSON.stringify({
      time: new Date().toISOString(),
      value: Math.random().toFixed(4),
    });

    res.write(`id: ${eventId}\n`);
    res.write(`event: tick\n`);
    res.write(`data: ${data}\n\n`);
  }, 1000);

  req.on("close", () => {
    clearInterval(interval);
    console.log("Client disconnected");
  });
});

app.listen(3000, () => {
  console.log("SSE server running on port 3000");
});

Client (Browser)

const source = new EventSource("/events");

source.addEventListener("tick", (event) => {
  const data = JSON.parse(event.data);
  console.log(`Event #${event.lastEventId}: ${data.time} - ${data.value}`);
});

source.addEventListener("open", () => {
  console.log("Connection established");
});

source.addEventListener("error", (event) => {
  if (event.target.readyState === EventSource.CLOSED) {
    console.log("Connection was closed by the server");
  } else if (event.target.readyState === EventSource.CONNECTING) {
    console.log("Reconnecting...");
  }
});

Notice how little code the client requires. The EventSource API handles connection management, reconnection, and event parsing for you.

WebSocket Code Example

Here is the equivalent real-time connection using WebSocket. You can test WebSocket connections with our browser-based tool.

Server (Node.js + ws)

const { WebSocketServer } = require("ws");

const wss = new WebSocketServer({ port: 3000 });

wss.on("connection", (ws) => {
  console.log("Client connected");

  const interval = setInterval(() => {
    if (ws.readyState === ws.OPEN) {
      const data = JSON.stringify({
        time: new Date().toISOString(),
        value: Math.random().toFixed(4),
      });
      ws.send(data);
    }
  }, 1000);

  ws.on("message", (message) => {
    console.log(`Received: ${message}`);
    // Echo the message back
    ws.send(`Echo: ${message}`);
  });

  ws.on("close", () => {
    clearInterval(interval);
    console.log("Client disconnected");
  });
});

console.log("WebSocket server running on port 3000");

Client (Browser)

let ws;

function connect() {
  ws = new WebSocket("ws://localhost:3000");

  ws.addEventListener("open", () => {
    console.log("Connected");
    ws.send("Hello from client");
  });

  ws.addEventListener("message", (event) => {
    const data = JSON.parse(event.data);
    console.log(`${data.time} - ${data.value}`);
  });

  ws.addEventListener("close", () => {
    console.log("Disconnected. Reconnecting in 3 seconds...");
    setTimeout(connect, 3000);
  });

  ws.addEventListener("error", (error) => {
    console.error("WebSocket error:", error);
    ws.close();
  });
}

connect();

Notice the manual reconnection logic in the client. You need to detect the close event and call connect() again with a delay. For production applications, you should add exponential backoff and a maximum retry count. Our JavaScript WebSocket guide covers these patterns in detail.

When to Use SSE

SSE is the better choice when data flows primarily from server to client:

  • Notifications and alerts. Push notifications to the browser as they happen. The client rarely sends anything back through the event stream.
  • Live feeds. News feeds, social media timelines, and activity logs update from the server. The client reads them.
  • Dashboard updates. Real-time metrics, charts, and monitoring dashboards receive data from the server and display it. User interactions (clicking a chart, changing filters) happen through normal HTTP requests.
  • AI streaming responses. Services like ChatGPT stream generated text token by token. The user sends a prompt via a POST request, and the response streams back over SSE. This pattern is becoming the standard for LLM APIs.
  • Stock tickers (read-only). Price updates flow from server to client continuously. If you only display prices and do not need to submit orders through the same channel, SSE handles this well.

The common pattern here is that the server pushes data and the client consumes it. Any client-to-server communication happens through separate, standard HTTP requests.

When to Use WebSocket

WebSocket is the better choice when you need bidirectional, low-latency communication:

  • Chat applications. Users send and receive messages constantly. Both directions need to be fast and efficient. A Node.js WebSocket server can handle thousands of concurrent chat connections.
  • Multiplayer games. Player inputs go to the server, game state comes back, all within milliseconds. The bidirectional nature of WebSocket is essential here.
  • Collaborative editing. Tools like Google Docs need to send user edits to the server and receive other users’ edits in real time. Operations flow in both directions with high frequency.
  • Trading platforms. Traders receive market data and submit orders. Both actions are time-sensitive and happen through the same connection to minimize latency.
  • IoT device communication. Devices send sensor data and receive commands. The server needs to push instructions to devices at any moment.

The common pattern here is that both the client and server are active participants in the conversation. Data flows frequently in both directions, and latency matters.

Can You Use Both?

Yes, and many production systems do. You are not forced to pick one protocol for your entire application. A practical architecture might look like this:

  • Use SSE for pushing notifications, live feed updates, and dashboard metrics to the browser. These are read-heavy streams where the client mostly listens.
  • Use WebSocket for the chat feature or collaborative editing component, where users actively send and receive messages.
  • Use standard HTTP requests for everything else: form submissions, API calls, file uploads.

For example, a trading platform might use SSE to stream market data to all connected clients (one-to-many, server-to-client) and WebSocket for the order book and trade execution (bidirectional, low-latency per user). The notification system runs on SSE because it only pushes alerts. The support chat runs on WebSocket because both parties type messages.

This hybrid approach lets you pick the simplest tool for each feature. SSE connections are cheap and easy to scale behind standard HTTP infrastructure. WebSocket connections require more server resources but give you the bidirectional channel when you actually need it.

You can also start with SSE and migrate specific features to WebSocket later if requirements change. Since SSE runs over HTTP, it fits into your existing architecture with no additional infrastructure. Adding WebSocket requires ensuring your load balancer, proxy, and firewall all support it, as described in the WebSocket vs HTTP comparison.

  • What Is WebSocket? covers the protocol in depth, including the handshake, frame format, and connection lifecycle.
  • WebSocket vs HTTP explains how WebSocket differs from traditional HTTP and when the upgrade is worth it.
  • JavaScript WebSocket Guide walks through building a production-ready WebSocket client with reconnection, heartbeats, and error handling.
  • Node.js WebSocket Server Guide shows how to build a WebSocket server with the ws library, including authentication and scaling.
  • WebSocket Tester lets you connect to any WebSocket endpoint from your browser and send messages interactively.