Chat: transport, persistence, ordering, acknowledgements, presence, offline sync
coreadvancedA chat system is a real-time delivery problem wrapped around a durable log, and the single most common design error is treating it as the reverse. The transport is a WebSocket, because chat needs server-initiated messages and a per-message HTTP request would carry more overhead than payload — but the connection is a delivery channel, never the record: a message is durable when it is written to storage, not when it is written to a socket. Ordering is per conversation, not global, so each message gets a sequence number assigned by the server within its conversation, and clients order by that rather than by their own clocks or by arrival. Delivery acknowledgements are three distinct facts people mistake for one — the server accepted it, the recipient's device received it, the recipient read it — and each needs its own record because each is visible in the product as a different tick. Presence is deliberately approximate: it is high-write, low-value data with a short TTL refreshed by heartbeats, and storing it durably is a common way to make a chat system expensive for no product gain. Offline sync is what makes the whole thing usable, and it is a pull, not a push: a reconnecting client sends the last sequence number it holds per conversation and receives everything after it, which is also exactly what a client that missed messages during a network blip needs. Fan-out to a group means resolving members to their live connections, which live on different servers, so a pub/sub layer routes each message to the servers holding those connections. And a push notification is the fallback for a recipient with no live connection — driven by the same delivery state, not by a separate code path.
Think of it as
Think of the conversation as an append-only log that happens to have a fast notification channel attached. The log is the truth: it has an order, it survives disconnection, and any client can catch up by asking for everything after position N. The WebSocket is a courtesy that saves clients from polling the log. When you are unsure how some feature should behave — a missed message, a reconnect, a device that was offline for a week, a group of 500 people — ask what the log says and how the client catches up to it. Almost every chat feature has a clean answer in those terms and a messy one in terms of sockets.
What we're doing: Follow one recipient through a disconnection and see how the log, not the socket, repairs it.
- 7
- Because the message was persisted before delivery was attempted, a dead socket costs nothing. Had the socket write been the only record, these three messages would exist only in the sender's client.
- 15
- The sync request carries a position, not a timestamp. A timestamp-based catch-up has to choose between re-sending messages the client already has and missing ones written slightly out of clock order — a sequence number has neither problem.
- 19
- The same request handles a two-second blip and a two-week absence. Designing catch-up as a position-based pull means there is no separate "long offline" code path to get wrong.
Why this works: Every hard part of chat — reconnection, multiple devices, ordering, read receipts — reduces to "what is the client's position in this log, and what comes after it". A design that treats the socket as the delivery record has to invent a separate mechanism for each of those cases; a design built on a sequenced, persisted log answers all of them with one request.
Treating a successful socket write as delivery
Wrong
Better
What you see: Messages show a delivered tick that the recipient never received, most often when their connection dropped moments earlier — the TCP buffer accepted the write, the socket had not yet noticed it was dead, and nothing ever corrected the state.
Why: A socket write succeeds when the data enters the local send buffer, which can happen well after the peer is unreachable. Delivery is a statement about the recipient, so only the recipient can make it — which is why the acknowledgement travels back from the device rather than being inferred at the sender.
- Sender → Chat server A: send(conversation, body)
- Chat server A → Message store: assign seq, persist (durable here, not before)
- Chat server A → Sender: accepted (seq 4471)
- Chat server A → Pub/sub: publish to conv:9812
- Pub/sub → Chat server B: route to servers holding member connections
- Chat server B → Recipient: deliver over live socket
- Recipient → Chat server B: delivered(4471), later read(4471)
- Chat server B → Recipient: no live socket → push notification instead
Three acknowledgements that are usually collapsed into one
Each requirement and where it is handled
Remember: Chat is a durable, per-conversation sequenced log with a fast delivery channel attached — not a socket with storage bolted on. Persist and sequence before delivering; order by server-assigned sequence number, never by client clocks; keep accepted, delivered and read as three separate acknowledgements; make presence a short-TTL heartbeat key rather than durable state; sync offline clients by pulling everything after their last sequence number; fan out to groups through pub/sub because connections live on different servers; and fall back to push when no live connection exists.
See also: connection affinity and registries · pubsub fanout across instances · choosing a transport · the cost of global ordering · separating intent from delivery · at most least exactly once

