WebSocket
Tool
A communication style that keeps a connection open so either side can send data to the other at any moment.
Why it exists
The web's default exchange asks once, answers once, and closes. The screen calls the server, the server responds, and that connection is gone. In that structure the server has no way to speak first. There is no channel for telling a screen that something changed.
Editing one document with two people made the limit obvious. Seeing what the other person changed means the screen has to keep asking. Ask every three seconds and the delay is three seconds; with a hundred people holding one document open, thirty requests a second travel even while nobody is typing. Shorten it to half a second and the delay improves while the requests multiply by six. Neither setting is acceptable for two people typing in the same paragraph.
A workaround held responses open for a while: the screen asks, the server withholds its answer, and returns it when something changes. The delay dropped, but a fresh connection was still established each time, and the equipment in between would close those connections arbitrarily, which made the whole thing awkward to operate.
So a protocol that establishes one connection, keeps that channel, and lets either side send at any time became standard. The first exchange starts as an ordinary web request; inside that request both sides agree to switch the channel, and from then on it stays open. Reusing the ordinary request for the handshake is what let this pass through existing web infrastructure.
What it solves
Three things change at once. The server can speak first, the cost of one exchange drops, and the delay falls to milliseconds — because the headers and the connection setup that used to accompany every request happen only on the first one.
Where it earns its keep is narrow and clear: a screen several people edit simultaneously, a feature showing another person's cursor live, conversation, an operations dashboard whose numbers keep moving. What these share is that both sides send often. Where only one side sends, the case for holding a channel open gets much weaker.
Keeping it open costs something on the operations side.
- Connection count is the resource. One server has a ceiling on how many connections it can hold at once, and that ceiling is a different kind of limit from how fast it processes requests. Capacity gets calculated from open connections rather than from active users. A server comfortable with a thousand requests a second may struggle with ten thousand idle connections.
- The server ends up holding state. Knowing which user is attached to which server is necessary, so adding a second server requires a separate mechanism to carry changes from someone on server one to someone on server two. A relay layer passing events between servers goes in between.
- Disconnection is normal, not exceptional. Using an app on a subway means the connection drops constantly. You need reattachment and a way to fill in changes missed while detached. Numbering each message and sending the last received number on reconnect lets the stream resume from there. Without the numbering, a reconnect silently loses whatever happened during the gap.
- Intermediate equipment closes quiet connections. Proxies and firewalls clean up connections with no traffic, so an empty signal every few tens of seconds announces that this one is alive.
- Authentication gets checked only once. It's verified when the connection is established and then the channel stays open, so somebody whose permission was revoked in the meantime can still be receiving. Rechecking periodically, or closing connections when permissions change, has to be added. Nothing about an open channel expires on its own.
Alternatives
Four approaches address the same need differently.
- Asking repeatedly at a short interval. The existing REST endpoint works as is, with nothing new to build, and it trades delay against request volume. Where a second or two of delay is fine, this is enough.
- Streaming one direction, server to screen. The connection stays open but only the server talks. The browser reconnects on its own and the implementation is simpler, so where the screen has almost nothing to send, this is the better fit.
- Exchanging between servers. A webhook, where the side something happened on notifies an address, holds no connection, which suits communication crossing a company boundary.
- Adding subscriptions to a query-shaped endpoint. GraphQL has receiving-on-change in its specification, and the channel underneath it is usually a WebSocket anyway.
Three questions decide it: whether both sides send, how often messages travel, and how many connections will be open at once. If only one side sends, the second approach is enough; if a few seconds of delay is fine, the first is enough.
How it sounds at work
"If it's only notifications, we don't need a WebSocket. Let's do one direction."
Don't choose something heavier than the need.
"We scaled to two servers and the other cursor stopped appearing. There's no relay."
Naming that connections are pinned to a server.
"On reconnect after the subway, edits from that gap vanish. Let's number the messages."
Treating disconnection as normal.
Pagedeck used this when it added simultaneous editing. Theo prototyped with three-second polling first, and two people editing the same paragraph kept colliding because for three seconds neither knew what the other had written. No amount of merge logic fixes a delay that large.
The operational work grew after the switch. Connections stay open for everyone holding a document, which changed the basis for capacity planning, and every deployment dropped all connections at once so reconnections arrived in a wave. Spreading reconnect timing out cost another two days, and none of that work was visible in the feature itself.
What people get wrong
The most common belief is that anything real-time calls for this. Its name and reputation invite that choice. But most of what gets called real-time in practice only needs the server to push toward the screen: notifications, progress, a badge for a new post. Opening a bidirectional channel brings connection management, reconnection, and a relay layer with it, and where one direction suffices only that burden remains.
The other frequent belief is that the job ends once the connection opens. Getting attached is a few lines from an example, so it reads that way. But most of the work with this style arrives after the connection is open. Filling gaps after a reconnect, keeping quiet connections from being closed, bridging between servers when you scale, dropping the connection of someone whose permissions changed — all of it comes afterward. Two days to connect means two weeks for the rest.
Easily confused with
| Term | When the exchange happens | How it differs from a WebSocket |
|---|---|---|
| Webhook | Once, when an event occurs | It holds no connection, which suits server-to-server traffic and especially traffic crossing a company boundary. To reach a screen immediately, the server receiving the notice still has to push it onward |
Where to go next
Last updated 2026-09-16