The Pitfalls of Two WebSocket Channels in WikiMind
In WikiMind, we split WebSocket usage between notifications and CRDT editing. Here’s where it’s easy to couple independent channels back together and lose predictability.

“If the bell breaks, will the document editor stop working too?” That question quickly took the romance out of the idea of “one WebSocket for everything.”
In WikiMind, we had two different real-time concerns. The first was notifications for the bell. The second was collaborative CRDT document editing. Both are convenient to run over WebSocket, but their requirements are different.
For the CRDT editor, the channel affects the working flow: the user types, receives operations from other participants, and the client needs to understand the connection state. For notifications, things are looser: the WebSocket acts as a push-only signal. If the user is offline, we do not try to catch them up over the socket. The message stays in the database and is read through REST.
We split this into two independent channels. Below are the pitfalls we almost ran into.
Pitfall 1: one socket looks simpler until a shared failure appears
At the start, the most obvious option is to keep a single WebSocket and distinguish messages by type: notification, crdt_operation, presence, and so on. Fewer connections, one reconnect, one error handler.
The problem is that this gives us a single point of failure for two different features. If the channel goes down because of something on the notifications side, collaborative editing loses its transport too. If the editor needs to reconnect or resync the CRDT state, the bell gets dragged into the same cycle.
This is especially unpleasant for the UI. A user may be reading a document without issues, but because of a problem in the notification flow, the editor starts looking “broken.” Or the other way around: active document editing affects the delivery of ordinary push events.
We split the WebSocket channels physically and by lifecycle:
- the notifications channel lives separately;
- the CRDT editing channel lives separately;
- one channel going down does not close the other;
- reconnect and error handling are not shared.
A practical detail: the biggest debate was not about the protocol, but about whether two connections for one application was overkill. In the end, the argument was simple: there is one more connection, but failure no longer spreads between scenarios.
Pitfall 2: it is easy to accidentally turn the notifications WebSocket into a queue
Notifications had a different trap. As soon as a WebSocket exists, it becomes tempting to treat it as guaranteed delivery: if the user was offline, then on reconnect we should resend everything they missed over the same channel.
But in WikiMind, we did not design that semantics in. Our notifications WebSocket is push-only. It reports an event if the user is online right now. If not, the push is lost. That is fine, because the source of truth is not the socket but the database.
Stored notifications can be read through REST. So after opening the interface or returning to the app, the client should not wait for a “history” from WebSocket. It fetches the current state with a normal request.
That way, we did not mix two delivery modes:
- WebSocket — a fast signal for an online user;
- database — notification storage;
- REST — reading the current list and missed messages.
The important point: losing a push event is not the same as losing the notification. Only the immediate signal to the bell is lost; the record remains available through REST.
Pitfall 3: independent channels can be accidentally coupled on the client
Even if the channels are separated on the server, it is easy to glue them back together in the frontend. For example, by making one shared connection manager, one global “WebSocket disconnected” status, and one “reconnect” button.
For WikiMind, that would have been harmful. If the notifications channel goes down, the editor should not receive an error state. If the CRDT channel reconnects, the bell does not have to start blinking in alarm.
So we split the states the same way we split the connections. Each channel has its own events, its own errors, and its own reconnect logic. What remains shared is only what is actually shared by the application: the user session and the ability to fetch data from REST.
That removed some strange edge cases. For example, a user can temporarily stop receiving push notifications and still keep editing a document. Or they can open the bell and read notifications through REST while the CRDT channel is recovering after a drop.
Where the boundary is
The practical rule ended up being this: if a failure in one real-time scenario should not break another, they do not belong in the same WebSocket channel.
In WikiMind, notifications and CRDT editing differ in criticality, offline behavior, and how data is restored. So two independent channels turned out to be simpler to operate than one universal transport with a pile of conditions.
The next step for setups like this is to explicitly define the contract for each channel: what it delivers, what it does not guarantee, where state is stored, and which request the client uses to recover after being offline.