Real-time web / Decision guide
SSE vs WebSockets: How to Choose for Live Dashboards and Tables
You need a dashboard that updates by itself. Somebody says "WebSockets", somebody else says "just use SSE", and both are right for a different screen. Here is how to tell which screen you have.
This guide compares Server-Sent Events and WebSockets for the most common real-time job on the web: a page that shows data and keeps it current. Live dashboards, admin tables, order boards, monitoring panels, AI responses that arrive token by token. If you want the protocol basics first, start with Server-Sent Events explained and come back.
The short answer
Use SSE when the server does most of the talking. Dashboards, feeds, notifications, progress bars, live tables and streamed AI output send many updates down and only an occasional command up. SSE handles the stream over plain HTTP, and the commands go up as ordinary requests.
Use WebSockets when both sides talk constantly. Chat with typing indicators, multiplayer games, collaborative editing and cursor sharing send frequent messages in both directions, sometimes binary. That is what a full-duplex connection is for.
Once a connection is open, either transport moves a small message to the browser quickly. The real differences sit around the connection: who may send, how it reconnects, what your proxies and load balancers have to support, and what your server runtime has to keep alive.
SSE vs WebSockets at a glance
| Question | Server-Sent Events | WebSockets |
|---|---|---|
| Direction | Server to browser. The browser sends with normal HTTP requests. | Both directions over one connection. |
| Protocol | A long-lived HTTP response with Content-Type: text/event-stream. | An HTTP handshake that upgrades to the WebSocket protocol (RFC 6455). |
| Payload | UTF-8 text: JSON, HTML fragments, plain strings. | Text or binary frames. |
| Reconnect | Built into EventSource, with Last-Event-ID sent back to the server. | Your code or a client library handles it. |
| Proxies and load balancers | Plain HTTP. Turn off response buffering on the way. | Every hop must pass the Upgrade handshake and keep long connections open. |
| Authentication | Same-origin cookies work as on any request. EventSource cannot set custom headers. | Cookies go with the handshake. The browser API cannot set custom headers either; check Origin on the server. |
| HTTP/2 and HTTP/3 | Multiplexed like any other request. | Classic WebSockets run over their own HTTP/1.1 connection; WebSockets over HTTP/2 need support on both ends. |
| Typical fit | Dashboards, feeds, progress, notifications, live tables, streamed AI output. | Chat, games, collaborative editing, high-frequency two-way traffic. |
Polling is still a valid third option when updates are rare and a few seconds of delay are fine. Asking an endpoint every thirty seconds is simpler than holding a connection open for a value that changes twice an hour.
The question that decides it: who talks?
Before comparing features, write down every message your screen sends and receives during a typical minute. Be literal.
If the arrows point mostly down, you are describing a subscription: the page wants to be told when something changes. That is the job SSE was designed for. If the arrows point both ways at a similar rate, every browser is also a publisher, and a single two-way connection starts paying for itself.
Most business screens land in the first group. A user sorts a table or approves an order a few times a minute. Those are commands, and HTTP already has a good way to send commands: a request that can be authorized, validated, logged and retried like every other request in your application.
SSE down, POST up: the pattern most screens need
The most common objection to SSE is "but my page also sends things". It usually does. That does not make it a WebSocket page. Split the traffic by its nature:
- Commands go up as HTTP requests. Change a filter, place an order, mark a ticket done. Each one is a normal
POSTwith your usual middleware, validation and error responses. - State comes down the stream. The result of a command, and everything that changed because of other users, arrives as events on one open SSE connection.
The command does not need to return the new data. It can return 202 Accepted, and the stream delivers the outcome to every page that cares, including the one that sent the command. This removes a whole class of bugs where the page that made a change shows one state and every other open page shows another.
AI interfaces use the same split. The prompt goes up as one request, and many model APIs stream the generated reply back as Server-Sent Events, a few tokens per event.
When you actually need WebSockets
WebSockets earn their extra moving parts when at least one of these is true:
- The browser sends many small messages per second. Cursor positions, keystrokes in a shared document, game input. One HTTP request per message would cost more than the message.
- Latency matters in both directions. A move in a game has to reach the other players as fast as their moves reach you.
- You send binary data. Audio chunks, compact binary protocols, file transfer over the live channel. SSE carries text only.
- You speak an existing WebSocket protocol. Many hosted real-time services and brokers already expose a WebSocket API, and fighting that rarely pays off.
The cost is mostly operational. Every proxy, load balancer and CDN on the path has to support the upgrade and long-lived connections. Reconnection, missed messages and heartbeats are your code's job. And a WebSocket endpoint is not covered by CORS, so the server has to check the Origin header itself to stop other sites from opening authenticated connections with your users' cookies.
A live table over SSE, frame by frame
Theory is cheap, so here is a real one. The SSE-driven data grid on this site is a trading tape that updates for every visitor while anyone places a trade. Open it in two tabs and watch the panel under the table: it lists every frame that crosses the wire.
The grid uses exactly the split described above:
- Its first request is
OPTIONSto the feed endpoint. The answer is the feed's contract: the fields, the pager, what may be sorted and filtered, and whether it can be served live. - It opens one SSE stream for the whole page and subscribes with its current view: page, sort and filters.
- Sorting or filtering sends the new view up as a small
POST. The new rows do not come back on that request. They arrive on the stream. - When a trade is placed anywhere, the server re-runs each subscriber's own view and sends a keyed patch with only the rows that moved. A visitor whose page did not change receives nothing.
event: ui.collection.patch
data: {"patch": {"key": "id",
"upsert": [{"id": "TRX-985F23", "asset": "gold", …}],
"remove": ["TRX-1A0C7E"],
"order": ["TRX-985F23", "TRX-4C2BBB", …]}}
Nothing in this exchange needed a two-way socket. The browser sends a handful of view changes per minute, and the server does the rest. The cost moved to the server instead: it holds one view per subscriber and re-runs it when the data changes. That trade-off is worth knowing about whichever transport you choose, and the grid article lists its costs and limits openly.
What changes when your backend is PHP
Most SSE vs WebSockets articles assume Node.js or Go, where a process holds thousands of open connections as a matter of course. Classic PHP works differently, and that changes the decision more than any protocol detail.
PHP-FPM: one worker per open stream
Under PHP-FPM, every request occupies a worker until it finishes. An SSE response never really finishes, so each open tab holds a worker for as long as it stays open. Fifty viewers of a dashboard can use up a pool sized for normal page traffic. SSE under FPM works for an internal tool with a few users; it does not scale to a public page.
WebSockets do not run under FPM at all. A WebSocket needs a process that keeps the connection after the handshake, so PHP teams run a separate server for it: Laravel Reverb, Ratchet on ReactPHP, a Swoole server, or a hosted service.
Hubs and long-running runtimes
Two ways out keep the application code simple:
- A separate hub. Your PHP code publishes an update, and a dedicated process holds the browser connections. Mercure, built into FrankenPHP and integrated with Symfony, does this over SSE. Laravel Reverb does it over WebSockets.
- A long-running runtime. Swoole, RoadRunner or FrankenPHP worker mode keep PHP in memory between requests, so the application itself can hold open streams cheaply with coroutines or event loops. The price is discipline: state that survives between requests is shared by whoever comes next. We wrote about that in long-running PHP.
Semitexa takes the second route. It runs on Swoole, serves SSE from the same application that renders the pages, and routes commands as ordinary payloads and handlers. The live grid above runs that way in production, with publishes crossing Swoole workers so the tab that places a trade and the tab that sees it do not need to share a process.
Production checklist for either transport
Check every hop between server and browser
For SSE, the usual failure is buffering: a proxy collects the stream and delivers it in one piece, so updates arrive late or all at once. With NGINX, disable proxy_buffering for the stream location or send X-Accel-Buffering: no. For WebSockets, NGINX needs proxy_http_version 1.1 and the Upgrade and Connection headers passed through, as described in the NGINX WebSocket proxying guide. In both cases, raise idle timeouts above your heartbeat interval.
Send heartbeats
Quiet connections get closed by proxies and mobile networks. For SSE, a comment line such as : ping every 15 to 30 seconds keeps the stream warm. For WebSockets, use ping frames or an application-level heartbeat.
Count connections per tab
Over HTTP/1.1, browsers allow only a few connections per origin, and MDN notes the low limit for EventSource in particular. One stream per widget runs out fast when users open several tabs. Serve SSE over HTTP/2 where you can, and share one stream per page between all live components.
Plan for the reconnect
Reconnecting is not recovering. EventSource reconnects on its own and sends the last event id, but the server decides what happens next: replay what was missed, or send the current state. For a table or a dashboard, resending the current view is usually simpler and always correct. With WebSockets, you build the same logic yourself, plus the reconnect.
Authorize the subscription, not just the page
A stream is access to data. Check permissions when the subscription starts, and decide what happens when a session expires while the connection is still open.
Common questions
Is SSE faster than WebSockets?
For delivering a message to an open page, neither has a meaningful speed advantage for typical dashboard traffic. WebSockets have less overhead per message in both directions, which matters for high-frequency two-way traffic. For a page that receives updates, the choice is about direction, infrastructure and recovery, not raw speed.
Can the browser send data over SSE?
Not over the same connection. EventSource only receives. The browser sends with ordinary HTTP requests, which is usually what you want for commands anyway. If you need to send a request body and read a streamed reply on the same request, fetch() can read a text/event-stream response as a stream, which is how many AI chat clients work.
Does SSE work over HTTP/2?
Yes. Over HTTP/2 each stream is multiplexed on a shared connection, which removes most of the per-origin connection pressure you hit with HTTP/1.1.
Can I use SSE or WebSockets with PHP-FPM?
SSE works but holds one FPM worker per open connection, so it only suits low traffic. WebSockets need a long-running process outside FPM. For anything public, use a hub such as Mercure or Reverb, or a long-running runtime such as Swoole, RoadRunner or FrankenPHP.
Should I start with polling?
If the data changes a few times an hour and a short delay is acceptable, yes. Polling has no open connections and no reconnect logic. Move to SSE when users notice the delay or when the polling requests start to cost more than the updates are worth.
New to the protocol itself? Server-Sent Events explained covers the event format, EventSource and reconnection step by step.