Want to hear about a feature I built twice?
I was working on a side project — a small dashboard I'd been tinkering with on weekends. It needed a real-time panel. When something happened on the server, the panel should update without a refresh.
The version I shipped first used WebSockets.
The setup
I'd done my homework. WebSockets are the canonical real-time technology. Every tutorial about server-to-client updates screams "WebSockets" from the rooftops. When you read about how Slack pushes messages or how Figma syncs cursors, the answer is WebSockets.
So, I reached for WebSockets.
I added a WebSocket server in my backend, set up the connection from the client, and then started filling in everything WebSockets doesn't give you for free.
The reconnect logic — wait one second, then two, then four, back off further, jitter so all my open tabs wouldn't slam the server when it restarted.
A heartbeat ping-pong, because WebSockets are silent. A dead connection looks healthy until you try to send something.
Authentication was the worst of it. The browser's WebSocket API won't let you set headers on the upgrade request. I was stuck between passing the token in the URL (ugly, logs everywhere) or doing a custom handshake message after the connection opened (more state, more surface area for bugs).
It worked. It worked the way a thing you've spent a lot of evenings on works — fragile in the corners, fine in the middle.
Then I learned something
I was reading an old Mozilla blog post about a different topic, and the author dropped a name I'd never used.
EventSource.
I looked it up. The concept is stupidly simple.
The server keeps an HTTP response open. The browser exposes that response as a stream of events. When the connection drops, the browser automatically reconnects. When it reconnects, the browser sends a Last-Event-ID header so you can resume where you left off.
I read that paragraph and thought: Wait, that's literally all I was trying to build.
I went and looked at the client-side code I'd written. The reconnect logic. The heartbeat. The auth handshake. Hundreds of lines.
Then I looked at what EventSource would have been:
const source = new EventSource('/api/notifications')
source.addEventListener('message', (event) => {
showNotification(JSON.parse(event.data))
})
That's it. The browser handles the rest.
What I changed
I rewrote it.
The protocol layer collapsed. No upgrade handshake, no frame format. Just a long-lived HTTP response with text/event-stream as the content type.
Auth went back to being normal. EventSource sends cookies, so the same middleware that protected my REST endpoints protected this one too.
Debugging got easier. I could curl the endpoint and watch events stream into my terminal instead of squinting at the devtools frame inspector.
The whole rewrite took one evening.
The question I wish I'd asked
Here's the question I wish someone had handed me before I started.
Which direction is the data flowing?
In my dashboard, the answer was obvious in retrospect.
Server to client. Always.
The user clicked, posted, submitted — but those were normal HTTP requests. The server had things to tell the user, and those were pushes.
I was never going to send anything through the WebSocket from the client. That bidirectional channel was a heavy, complex tool I wasn't even using.
Most "real-time" features look like that. Dashboards, logs, build progress, notifications panels — all server to client. The features where WebSockets actually earn their cost are narrower than people assume. Chat with sub-second propagation. Collaborative editing. Multiplayer games.
Things where the round trip is the product.
Most of us don't build those.
What I'd say to past-me is to spend ten minutes thinking about the data flow before picking the protocol. If it's one-way, there's a four-line API that already solves your problem.
An evening's work instead of weeks.
Same feature.
Top comments (0)