Site icon NexGismo

SSE vs WebSockets: Which to Use for Real-Time Apps in 2026

SSE vs WebSockets: Which to Use for Real-Time Apps in 2026
TL;DR
  • What: SSE streams text from server to browser over HTTP; WebSockets open a two-way channel.
  • Why it matters: Picking the wrong one adds proxy config, auth workarounds and scaling pain you didn’t need.
  • What to do: Default to SSE for one-way updates and AI streaming; use WebSockets only for frequent two-way messages.
  • Key stat: Over HTTP/1.1, browsers allow just 6 SSE connections per domain, shared across all tabs.

SSE vs WebSockets comes down to direction. Server-sent events push one-way text updates from server to browser over ordinary HTTP, with automatic reconnection and resume built in. WebSockets upgrade a connection into a full-duplex channel for frequent messages in both directions. Use SSE for notifications, dashboards and AI token streams; use WebSockets for chat, games and collaborative editing.

Server-sent events (SSE) is a W3C/WHATWG browser API, exposed as EventSource, that receives a stream of UTF-8 text events from a server over a long-lived HTTP response with the text/event-stream type. WebSocket is an IETF protocol, standardised as RFC 6455 in December 2011, that turns an HTTP connection into a persistent, bidirectional, message-framed channel.

The SSE vs WebSockets question comes up the moment a product manager asks for “live” anything: a notification bell, an order tracker, or an AI chat box that types its answer out. Many teams reach for WebSockets by reflex. Then they spend days on Nginx upgrade headers, sticky sessions and a separate socket server. Others pick SSE and hit a different wall: a seventh browser tab that never loads, or a PHP-FPM pool with no free workers.

This comparison covers how each protocol works and where each breaks in production. It also shows what the major AI APIs and the Model Context Protocol chose. You’ll get working PHP and Node.js code, the proxy settings that matter, and a verdict. If you’re building the event pipeline behind the stream, our guide to event-driven architecture in PHP covers the producer side.

What is the difference in SSE vs WebSockets?

The core difference in SSE vs WebSockets is direction and framing. SSE is a one-way stream of text events over a normal HTTP response. WebSockets are a two-way channel with their own binary framing after an HTTP upgrade.

With SSE, the browser opens a GET request and the server never finishes the response. It writes blocks like data: ... followed by a blank line, and the browser fires an event for each block. The WHATWG spec defines the format. Event streams are always decoded as UTF-8, so binary data must be encoded as text first.

With WebSockets, the client sends an HTTP GET with Upgrade: websocket. The server answers 101 Switching Protocols, and from then on both sides exchange frames. RFC 6455 requires that a client MUST mask all frames it sends, using a 4-byte masking key. Each frame header runs from 2 to 14 bytes depending on payload length and masking.

That framing makes WebSockets flexible. It is also why every proxy, load balancer and CDN in the path must understand the upgrade. SSE needs nothing beyond HTTP.

When should you use SSE instead of WebSockets?

Use SSE when the server talks and the client mostly listens. That covers more real features than most developers expect.

SSE gives you three things for free that WebSocket code must build by hand. First, automatic reconnection: the WHATWG spec says clients will reconnect if the connection is closed. A server can stop that with an HTTP 204 response. Second, resume: when events carry an id, the browser sends a Last-Event-ID header on reconnect. The server can then replay what was missed. Third, browser support: EventSource has been available across browsers since January 2020 according to MDN’s Baseline data.

Pair resumable IDs with an event log and you get at-least-once delivery. On the client side, de-duplicate by ID, the same idea covered in our idempotency keys guide.

My recommendation: if you can describe the feature as “the server tells the user when something changes”, start with SSE.

When are WebSockets the better choice?

WebSockets win when both sides send frequent, small messages and latency matters in both directions. Chat with typing indicators, multiplayer games, collaborative cursors and live auctions all fit that pattern.

They also win for binary payloads. SSE is text-only, while WebSocket frames can carry binary data without base64 overhead. OpenAI’s Realtime API is a good example. Its WebSocket guide describes sending and processing Base64-encoded audio chunks, with events flowing both ways. Even there, OpenAI recommends WebRTC rather than WebSockets for browser and mobile clients.

WebSockets used to have an HTTP/2 problem, because HTTP/2 has no Upgrade header. RFC 8441, published in September 2018, fixed that with an extended CONNECT method. A WebSocket can now run on one stream of a shared HTTP/2 connection. Server and proxy support for it still varies, so check your stack before you count on it.

How does SSE vs WebSockets look in PHP and Node.js code?

Both are short to write. The production work is in buffering, timeouts and process models, which the next section covers.

SSE endpoint in PHP

<?php
// sse.php - stream order status updates
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // tell Nginx not to buffer this response

$lastId = (int)($_SERVER['HTTP_LAST_EVENT_ID'] ?? 0);
echo "retry: 5000\n\n"; // reconnect delay in ms

while (!connection_aborted()) {
    foreach (fetchEventsAfter($lastId) as $event) { // your DB/Redis query
        echo "id: {$event['id']}\n";
        echo "event: order-status\n";
        echo 'data: ' . json_encode($event['payload']) . "\n\n";
        $lastId = $event['id'];
    }
    echo ": keep-alive\n\n"; // comment line, ignored by the browser
    @ob_flush();
    flush();
    sleep(2);
}

Browser client

const source = new EventSource('/sse.php');
source.addEventListener('order-status', (e) => {
  const update = JSON.parse(e.data);
  renderStatus(update);
});
source.onerror = () => console.warn('SSE dropped, browser will retry');

WebSocket server in Node.js

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
  socket.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    // broadcast to everyone else in the room
    for (const client of wss.clients) {
      if (client !== socket && client.readyState === 1) client.send(JSON.stringify(msg));
    }
  });
});

In Laravel you would normally not hand-roll the socket server. Laravel Reverb is a first-party WebSocket server that plugs into Laravel broadcasting and starts on 0.0.0.0:8080 by default. Python teams have similar options: Django Channels for WebSockets, or a streaming response for SSE.

What breaks SSE and WebSockets in production?

Most real-time bugs are infrastructure bugs. Here are the ones the official docs warn about.

Proxy buffering and idle timeouts

Nginx buffers upstream responses by default: the proxy module ships with proxy_buffering on. A buffered SSE stream arrives in bursts or not at all. Send X-Accel-Buffering: no from your app, as MDN’s PHP example does, or turn buffering off for that location.

Idle connections die too. The default proxy_read_timeout is 60s, measured between two reads. A quiet stream or socket gets cut after a minute. For SSE, the WHATWG authoring notes suggest sending a comment line every 15 seconds or so. For WebSockets, send ping frames or raise the timeout.

The browser connection cap

This one catches SSE users. MDN warns that over HTTP/1.1 the limit is 6 connections per browser and domain, shared across every tab. Chrome and Firefox marked the bug “Won’t fix”. Over HTTP/2 the client and server negotiate stream limits, which default to 100. Serve SSE over HTTP/2 or HTTP/3, or share one stream across tabs with a SharedWorker.

Auth headers

The native EventSource constructor accepts a URL and one option, withCredentials. You cannot set an Authorization header. Use cookie auth or a short-lived token in the URL. Or skip EventSource and read the stream with fetch(), which also lets you POST a body.

Open file and port limits

Every long-lived connection holds a file descriptor. Reverb’s docs note that its default stream_select event loop is typically limited to 1,024 open files. Past about 1,000 concurrent connections, you need the ext-uv loop. They also show a default Linux port range that caps one server at 28,231 connections. That is why Reverb scales out with Redis pub/sub.

Why do AI APIs and MCP stream with SSE?

AI APIs chose SSE because a model response is a one-way stream that follows a single request. You POST the prompt, then read tokens until the stream ends.

Anthropic’s Messages API streams using server-sent events when you set "stream": true. It sends named events such as message_start, content_block_delta and message_stop, plus periodic ping events. OpenAI’s streaming guide likewise covers HTTP streaming over server-sent events for the Responses API. It also lists a separate WebSocket mode for persistent sessions.

The Model Context Protocol went through the same decision in public. Its Streamable HTTP transport replaces the HTTP+SSE transport from protocol version 2024-11-05. Each client message is its own POST. The server answers with plain JSON or an SSE stream scoped to that request. Resumption uses the same Last-Event-ID header that browsers send. Our PHP Fibers guide is a useful companion if you’re consuming several of these streams concurrently from PHP.

Does PHP-FPM work for SSE vs WebSockets at scale?

PHP-FPM can serve SSE, but it scales poorly for long-lived streams. It cannot host a WebSocket server at all. This is the gotcha most SSE vs WebSockets comparisons skip.

PHP-FPM runs one request per worker. An SSE loop like the one above holds its worker for as long as the tab stays open. Your pm.max_children setting quietly becomes your maximum number of live viewers, and normal page requests queue behind them. Raising the pool size just trades the problem for memory pressure.

The fixes, in rough order of effort:

  1. Keep streams short. An AI response that ends in under a minute is fine on FPM.
  2. Put SSE endpoints in a separate FPM pool so they cannot starve the main site.
  3. Move long-lived streams to a long-running server such as Reverb, Swoole, FrankenPHP, or a small Node.js service.
  4. Offload fan-out to a managed push service and keep PHP for the business logic.

Node.js, Go and async Python avoid this. One process holds many idle connections on an event loop. The limits there are file descriptors and memory, not workers.

SSE vs WebSockets: protocol and production comparison (sources linked in the text above)
Factor Server-sent events (SSE) WebSockets
Direction Server to client only Full duplex, both directions
Standard WHATWG HTML Living Standard IETF RFC 6455 (Dec 2011); RFC 8441 for HTTP/2
Data format UTF-8 text only Text or binary frames
Reconnect and resume Built in, via retry and Last-Event-ID Manual, in your code
Custom auth headers in browser No (only withCredentials) No custom headers in the browser API either; use cookies, tokens or subprotocols
Browser connection cap 6 per domain over HTTP/1.1; negotiated (default 100) over HTTP/2 Not bound by the same per-domain HTTP/1.1 cap
Proxy setup Disable buffering, send keep-alive comments Forward Upgrade/Connection headers, raise idle timeout
Used by Anthropic and OpenAI streaming, MCP Streamable HTTP OpenAI Realtime (server-to-server), Laravel Reverb

The NexGismo verdict: SSE should be your default for real-time features in 2026, and WebSockets should be a deliberate upgrade. Most “live” features are one-way, and SSE gives you reconnection, resume and plain-HTTP deployment without a second server. I’d reach for WebSockets only when users send frequent messages back or you need binary frames. In Laravel, I’d use Reverb rather than build it myself.

Frequently Asked Questions

Is SSE better than WebSockets?

Neither is better in general. SSE is the better fit when data flows only from server to client, such as notifications, dashboards and AI token streaming, because it is plain HTTP with built-in reconnection. WebSockets are the better fit when both sides send frequent messages, such as chat, multiplayer games or collaborative editing.

Can SSE send data to the server?

No. Server-sent events are one-way: the server pushes text events to the browser over a single HTTP response. When the client needs to send something, it makes a normal HTTP request such as a POST or fetch call. Most AI chat apps work exactly this way: POST the prompt, then read the streamed answer as SSE.

Does ChatGPT use SSE or WebSockets?

OpenAI documents its standard streaming for the Responses API as HTTP streaming over server-sent events. Its Realtime API, used for low-latency voice, is a separate product that connects over WebSocket for server-to-server integrations, and OpenAI recommends WebRTC for browser and mobile clients.

What is the 6 connection limit for SSE?

Over HTTP/1.1, browsers cap open connections per domain at six, and that cap is shared across every tab, so a seventh SSE stream to the same site will hang. MDN notes the limit does not apply in the same way over HTTP/2, where the server and client negotiate concurrent streams, with a default of 100.

Can you use SSE with PHP?

Yes. A PHP script can send the text/event-stream content type, echo events and flush output, and MDN’s own SSE example is written in PHP. The catch is PHP-FPM: each open stream keeps one worker busy for as long as the browser stays connected, so long-lived streams need a separate pool or a long-running server.

Choosing between SSE vs WebSockets is mostly about being honest about direction. If the server does the talking, SSE gets you there with less code, less infrastructure and built-in recovery. Save WebSockets for features that truly need a two-way pipe. Whichever you pick, test it behind your real proxy and CDN with several tabs open. That’s where both protocols fail first. For more on keeping shared code readable as these features grow, see writing code like a human will maintain it. Got a real-time setup that surprised you in production? Share it with us and it may shape a future NexGismo post.

Sources

Exit mobile version