Realtime with WebSockets
Why request/response can't push updates to a client, how the WebSocket handshake upgrades an HTTP connection, and where realtime would sit next to Linkstash.

Every route in this series so far has followed the same shape: the client asks, the server answers, the connection closes. That model can't do one specific thing, tell the client something changed without being asked. If Sam saves a link and Aarav has Linkstash open in another tab, nothing about GET /links makes Aarav's screen update on its own. Someone has to ask again, and the honest options for "ask again" are all worse than they sound.
Why polling doesn't scale, and WebSockets exist
The obvious fix is polling: have the client call GET /links every few seconds and diff the result. It works, and it's also wasteful in a specific way, almost every one of those requests returns "nothing changed," full HTTP overhead (headers, a new connection or at least a round trip) spent to learn nothing. Poll every 2 seconds across a thousand open tabs and that's 30,000 requests a minute answering "no update" over and over.
A WebSocket solves this by turning one HTTP connection into something that stays open in both directions. The client doesn't ask repeatedly, it opens the connection once, and the server pushes a message down that same connection the moment something actually changes. No polling interval to tune, no wasted requests, and updates arrive in milliseconds instead of however long your poll interval was.
The handshake: an HTTP request that changes its mind
A WebSocket connection starts as an ordinary HTTP request, which is worth knowing because it explains why WebSockets work through the same ports, proxies, and infrastructure as everything else in this series:
GET /realtime HTTP/1.1
Host: api.linkstash.dev
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13The Upgrade: websocket header is a request to switch protocols mid-connection. If the server agrees, it answers with 101 Switching Protocols, and from that point on, the same TCP connection stops speaking HTTP and starts speaking the WebSocket protocol instead, a lightweight framed format built for sending small messages back and forth without HTTP's per-request overhead.
That 101 is a real, specific HTTP status code, not something WebSockets invented on their own, it's the same status line mechanism every response in this series has used, just for a request that's asking to stop being HTTP.
What this would look like for Linkstash
Nothing you've built across this series has a WebSocket route (Linkstash's tested contract is deliberately request/response only), but it's worth sketching what adding one would take, because the shape carries over from everything you already know. Using Node's ws package alongside the existing Express app:
import { WebSocketServer } from "ws";
import { createServer } from "node:http";
const server = createServer(app); // the same Express app from earlier lessons
const wss = new WebSocketServer({ server });
wss.on("connection", (socket, req) => {
const userId = authenticateFromQueryToken(req); // same idea as requireAuth
if (!userId) {
socket.close(4001, "Unauthorized");
return;
}
socket.send(JSON.stringify({ event: "connected" }));
});
// Called from inside POST /links, after createLink succeeds
function broadcastLinkCreated(link: Link) {
for (const socket of connectedSockets(link.userId)) {
socket.send(JSON.stringify({ event: "link.created", link }));
}
}The pattern is familiar even though the transport isn't. Authentication still happens before you trust the connection, same idea as requireAuth, just checked once at connection time instead of on every message, because the connection itself stays open. And the trigger for a push still lives at the same place a write already happens, POST /links, it just gains one more step after createLink succeeds: tell whoever's listening.
A WebSocket connection needs its own auth check
An HTTP request carries its Authorization header on every single call, so requireAuth runs fresh each time. A WebSocket connection is authenticated once, at the handshake, and then trusted for as long as it stays open, sometimes hours. That means a stale or revoked session token matters more here, not less: if you'd revoke a session (see sessions vs JWT), you need to actually close any WebSocket connections tied to it too, not just block new HTTP requests.
When you'd actually reach for this
Not every feature needs realtime. A dashboard nobody's staring at benefits little from push updates over a page refresh, and WebSockets add real cost: the server has to hold a connection, and therefore some memory, open per active client, for as long as they're connected, which is a different scaling problem than stateless HTTP requests that come and go. Reach for it when the delay of "ask again" is the actual product experience, chat, live collaboration, a dashboard someone's watching in real time, a multiplayer anything. For most CRUD APIs, including Linkstash as built in this series, request/response is not a limitation to work around, it's the right tool.
Quick check
What HTTP status code does a server return to accept a WebSocket upgrade request?
One lesson left, and it's not a new concept, it's the whole app you've been building piece by piece, put back together. Next: the Linkstash API end to end.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


