Most apps don’t need WebSockets.
Server-Sent Events runs over plain HTTP, reconnects itself, traverses firewalls cleanly, and ships in every browser. Mercure builds what SSE deliberately leaves out: authenticated publishing, topic filtering, and replay.
WebSocket: both ways always.
SSE: just outbound.
Two different shapes of network, two different operational profiles.
Persistent socket. Custom framing protocol.
Both sides can send anytime — even when nobody needs to.
Long-lived HTTP response, text/event-stream.
Browser auto-reconnects and replays via Last-Event-ID.
Which one for your stack?
Most real-time UI is 95% one-way.
Status badges, progress bars, live order books, streaming LLM tokens, push notifications, live CRUD lists. Every one of these is server-to-client. Outbound, from the user’s perspective, is just an HTTP request: a POST, a PUT, a DELETE. You’re paying for a full-duplex socket and only using half of it.
HTTP can stream. It always could.
The browser-native EventSource API opens a long-lived HTTP response and reads events as they arrive. Plain text over the wire, no framing protocol, no SDK to ship.
- One direction over plain HTTP. The server holds the response open and writes events as they happen.
EventSourceships in every browser. No SDK on the web.- Auto-reconnect is part of the standard. The browser reconnects on its own and replays via
Last-Event-ID. - Compatible with HTTP/2 multiplexing. One TCP connection carries many streams. The old “6 connections per origin” objection went away with HTTP/2, deployed everywhere since 2018.
Mercure adds what SSE leaves out.
SSE deliberately doesn’t ship publish authorisation, topic filtering, fan-out, or persistence. That’s exactly what Mercure adds — a single Go binary in front of your app, speaking the same plain HTTP your backend already speaks. JWT-authorised publishing, URI-template topic matchers, Redis/PostgreSQL transports for HA, history replay via Last-Event-ID. Run the OSS hub yourself, or use the EU-hosted Cloud.
And “bidirectional” stays cheap. Subscribe is a GET /.well-known/mercure the hub holds open; publish is an authenticated POST to the same endpoint. A browser whose JWT allows it can POST too — so you get WebSocket-style two-way messaging without a stateful socket per client.
Operational properties, line by line.
Neutral comparison. Both protocols work; they have different operational profiles.
| Property | Mercure / SSE | Raw WebSockets |
|---|---|---|
| Connection type | HTTP request kept open, multiplexed over HTTP/2+ | Stateful TCP connection upgraded from HTTP |
| Browser API | EventSource, native, no SDK | WebSocket, native, but you ship an SDK to layer auth, replay, and topic logic on top |
| Reconnect with replay | Standard. Last-Event-ID is in the HTML spec. | Application-defined; your SDK implements it |
| Authentication | Standard HTTP auth, JWT, cookies; works with every proxy and load balancer | Per-message or query-string token; no standard |
| Through proxies & corporate networks | HTTP requests survive most middleboxes | Some middleboxes buffer or drop the upgrade handshake |
| Mobile battery impact | Lower; no socket keep-alive on the client | Higher; sockets need keep-alive pings |
| Compression | Standard HTTP compression (gzip, br, zstd via Caddy) | Per-frame permessage-deflate; not always on |
| Horizontal scaling | Stateless front, shared transport (Redis, PostgreSQL) at the hub layer | Sticky routing or shared pub/sub layer; same problem, more state |
| Binary payloads | Via Base64 (≈33% overhead) | Native binary frames |