Production notes
Things worth deciding before you put a channel in front of real users.
Treat channel names as credentials
There is no authentication. Anyone with the name has full read and write access, forever, because names never expire.
- Generate them with a CSPRNG:
crypto.randomUUID()in a browser,crypto.randomUUID()in Deno and Node,secrets.token_urlsafe(32)in Python. - Never derive them from something enumerable — a user id, an email, an incrementing number, a date.
- Remember they end up in URLs, so they leak through screenshots, screen shares, browser history, referrer headers and chat logs. That is usually fine for a presentation; it is not fine for anything regulated.
- Rotate by simply agreeing on a new name. There is nothing to revoke.
If you need real authorization, put your own service in front and have it hand out short-lived channel names to authenticated users. The router then becomes a transport detail rather than the security boundary.
Do not put sensitive data on a channel
Transport is TLS-encrypted end to end, but the router process sees plaintext and so does redis. For anything sensitive, encrypt in the client and let the router move ciphertext it cannot read:
// both peers already share a key, out of band
send(JSON.stringify({ iv, ciphertext }));Always reconnect
Sockets drop: deploys restart every instance, mobile networks change, laptops sleep, proxies time out idle connections. A client without reconnection logic will silently stop working and users will call it a bug in your app.
Reconnect with backoff, and re-request state after reconnecting — you were disconnected, so you missed things, and nothing is replayed for you.
ws.addEventListener("open", () => {
emit("get-state", {}); // catch up on whatever happened while we were away
});Send keepalives
Idle connections get closed by intermediaries. A ping every 30 seconds is cheap insurance:
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send("ping");
}, 30_000);Throttle high-frequency sources
Pointer moves, device orientation and animation frames all fire far faster than anyone can perceive. Coalesce to 10–20 Hz before sending. Every message crosses redis and is delivered to every peer, so the cost is multiplied by the number of listeners.
Keep payloads small
Kilobytes, not megabytes. Put large or binary content in object storage and send a URL. Remember binary frames are not routed — base64 costs 33% on top of an already large payload.
Design for messages that never arrive
There are no acknowledgements. Design so that a lost message is recoverable:
- Prefer idempotent state updates ("the value is now 42") over increments ("add one"), so a repeat or a loss self-corrects on the next update.
- Re-send current state periodically if it matters, rather than only on change.
- Use request/response with a timeout when you genuinely need to know something got through.
Ignore what you do not recognise
Channels are shared with older versions of your own app, other tabs, and anything else that has the name. Parse defensively, ignore unknown message types, and never assume a payload is well-formed JSON.
Watch out for the ping prefix
Any message starting with ping is swallowed by the server as a keepalive and never reaches the channel. If you are relaying user-supplied text, wrap it in JSON rather than sending it raw. See messages.
Have a fallback
The router has no SLA and channels are ephemeral. For anything that must keep working, make the realtime path an enhancement over a polling or on-demand path rather than the only way your app gets data.