TypeScript and Socket.IO messaging platform
Real-Time Chat: Presence, Fan-Out and Backpressure
Building a real-time messaging platform on Node.js and Socket.IO, and the state-ownership problems that surface when you scale past one process.
- Transport
- WebSocket + Redis adapter
- Presence
- TTL-based leases
- Auth
- Handshake middleware
- Status
- Live demo available
Role
Full-stack design and implementation
Timeline
2026
Stack
Node.js, TypeScript, Socket.IO, Redis, MongoDB
Overview
A real-time messaging platform built on Node.js, TypeScript and Socket.IO, with room-based conversations, presence indicators and message history. The client is a React application; the server is a typed Express and Socket.IO process backed by MongoDB for persistence and Redis for coordination.
I wrote about the general lessons from this build in more depth in my article on scaling WebSockets. This case study covers the specific decisions in this codebase.
The problem
Chat is deceptively easy to prototype. Socket.IO's own tutorial gets you a working room in about thirty lines. The difficulty is that almost every one of those thirty lines assumes a single process holding all connections in memory — an assumption that fails the moment you want redundancy, let alone scale.
Architecture
The design deliberately keeps no authoritative state in process memory:
- Message fan-out goes through the Redis adapter, so any server can deliver to a client connected to any other server.
- Presence lives in Redis as a set of socket IDs per user, with a TTL refreshed by heartbeat.
- Message history is persisted to MongoDB and paginated on join, so a reconnecting client recovers what it missed.
- Authentication happens in handshake middleware, before the connection is registered.
io.use(async (socket, next) => {
const token = socket.handshake.auth?.token;
if (!token) return next(new Error("unauthorized"));
try {
const claims = await verifyJwt(token);
socket.data.userId = claims.sub;
next();
} catch {
next(new Error("unauthorized"));
}
});Technical challenges
Presence that survives a crashed process
The first presence implementation was a boolean set on connect and cleared on disconnect. It drifted within days, because a process that is killed never runs its disconnect handler and leaves users permanently 'online'.
Modelling presence as an expiring lease fixed it structurally rather than defensively. A user is online because a socket recently asserted it; if the asserting process dies, the key expires on its own. Storing a set of socket IDs rather than a flag also handles the multi-device case, where closing one tab should not mark someone offline on their phone.
Ordering under concurrent sends
Two messages sent milliseconds apart from different clients can arrive at different servers and be persisted out of order, so clients disagree about the sequence. Wall-clock timestamps do not resolve it — server clocks drift.
Ordering is established at persistence time by a monotonic per-room sequence, and clients sort on that rather than on timestamps. The message a client renders optimistically is reconciled against the authoritative sequence when the server acknowledges it.
Not dying under slow clients
Writes to a socket whose peer has stopped reading queue in the server's send buffer. Enough of those and heap grows, garbage collection pauses stretch, and healthy clients start seeing latency caused entirely by unhealthy ones.
Ephemeral events — typing indicators, presence changes — are dropped rather than queued when a socket's buffer exceeds a threshold. Losing a typing indicator is invisible; losing the server is not. Durable messages are not subject to this, because they are recoverable from history on reconnect.
Outcome
The result is a messaging layer that behaves correctly across multiple processes, recovers cleanly from server restarts, and degrades gracefully rather than catastrophically when individual clients misbehave.
The transferable lesson was recognising which state I had assumed was global. Room membership, presence and rate-limit counters all looked like application logic and were actually infrastructure concerns.
Published by Cenedy Udoy Palma.