The moment we hear “real-time”, we reach for WebSocket. After using both on my last three projects, here’s what I learned: more than half the time, Server-Sent Events are the better call. Simple, one-way, running over HTTP, passing cleanly through proxies. You don’t need WebSocket’s heavier setup.
What is SSE?
The server sends messages to the client over a long-lived HTTP connection. Messages come through in data: ...nn format. The client listens with an EventSource object.
const source = new EventSource('/events');
source.onmessage = (e) => console.log(e.data);
source.addEventListener('order-update', (e) => handleOrder(JSON.parse(e.data)));On the server side (PHP example):
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
while (true) {
if ($event = fetchEvent()) {
echo "event: order-updaten";
echo "data: " . json_encode($event) . "nn";
ob_flush(); flush();
}
sleep(1);
}WebSocket vs SSE
| Feature | SSE | WebSocket |
| — | — | — |
| Direction | One-way (server to client) | Two-way |
| Protocol | HTTP | TCP upgrade, WS/WSS |
| Reconnect | Automatic (browser handles it) | Manual |
| Proxy compatibility | Excellent | Medium |
| HTTP/2 multiplexing | Supported | Separate connection |
| Browser limit | 6 per origin | 200+ per origin |
| Format | Text (UTF-8) | Binary plus text |
The one-way constraint sounds limiting, but it’s the bread and butter case. Notifications, live dashboards, order tracking, stock tickers, log streaming. None of these need client-to-server writes on the same channel. SSE is enough.
Project example 1: shipment tracking
For a logistics company we built a shipment status dashboard. A user watches 50 to 100 shipments and wants the UI to update as statuses change. The first design was WebSocket. Once we went to production the problems started: customers’ corporate firewalls were blocking the WebSocket upgrade packets. We switched to SSE and the same firewalls let it through as plain HTTPS. One change.
Project example 2: chat app
I tried SSE for a messaging app and the lack of two-way hurt. A separate POST for every outgoing message, SSE for every incoming one. It works, but the tuning is endless. WebSocket was cleaner.
On top of that, typing indicators, read receipts, and presence all needed to ride the same channel. The SSE plus POST combo is too noisy for that.
SSE gotchas
- The HTTP/1.1 connection limit. Browsers allow 6 HTTP connections per origin. Six SSE streams and every other request is blocked. HTTP/2 solves this cleanly because it multiplexes.
- Proxy buffering. Nginx buffers by default, which holds the SSE stream and flushes it in batches. You need
X-Accel-Buffering: noon the response, orproxy_buffering off;on the server. - PHP-FPM doesn’t like long-lived connections. A worker process is locked for that one user. Under concurrency PHP-FPM runs out of workers. The fix is to write your SSE endpoint in ReactPHP, Swoole, or Node.js, and run it as a separate service.
- You need heartbeats. On long idle connections, proxies or load balancers close the socket. Send a comment message like
: heartbeatnnevery 20 to 30 seconds to keep the connection alive. - Last-Event-ID. On reconnect the browser sends a
Last-Event-IDheader with the last event it saw. The server can resume from there. Wire that up and clients reconnect without dropping messages.
Which one, when?
- User only needs to listen for data: SSE.
- Two-way interaction: WebSocket.
- Customers behind corporate firewalls: SSE is the safer bet.
- You have HTTP/2: SSE is comfortable.
- PHP backend with no option to stand up a separate service: polling, or WebSocket through Pusher/Ably.
- High-frequency binary data: WebSocket.
Start simple, move to WebSocket when you actually need it. For most real-time needs, SSE is both easier and cheaper.