How to Resume Missed Messages After a Reconnect
When a real-time client reconnects, resume from a cursor and a durable log. Do not reload everything. Learn how to catch up on missed messages precisely.
When a real-time client loses its connection and comes back, it has a hole: everything that happened while it was offline. The right way to fill that hole is with a cursor and a durable log. The client remembers the ID of the last message it saw, and on reconnect it asks the server for everything after that ID. The server replays the gap from a stored log and the client is caught up, precisely, with no duplicates and nothing missed. The wrong ways, reloading the entire history or just resuming live and silently losing the gap, are the two mistakes I see most, and both are avoidable with one small piece of state: a last-seen cursor.
Why "just resume live" loses data
The tempting shortcut is to reconnect the socket and start receiving new messages again. It works in a demo because in a demo the disconnect is instant and nothing happens during it. In reality, a disconnect can last seconds or minutes, and things happen during those seconds. If the client just resumes the live stream, everything sent during the outage is gone, because live delivery only carries what is happening now. The user comes back to a conversation with a hole in the middle that nobody will ever notice until they scroll up and find it.
This is the flip side of reconnection handling. Getting the socket back is necessary but not sufficient; I covered the connection mechanics in handling reconnection in real-time apps. Reconnecting the pipe does nothing about the data that flowed while the pipe was broken. That is a separate problem, and it needs a separate mechanism.
The cursor: remember the last thing you saw
The mechanism is a cursor, which is just the ID of the last message the client successfully processed. This is where server-assigned, ordered, gap-free IDs pay off, the same monotonic sequence numbers I argued for in guaranteeing message order in a chat app. Because messages have a strict order and stable IDs, the client can hold onto exactly one number, "I have everything through 40," and that single value describes its entire position.
On reconnect, the client sends that cursor: give me everything after 40. The server looks up 41 onward and replays it. The client appends, updates its cursor, and is now current. No full reload, no guessing, no gap. The cursor turns catch-up from a vague "resync everything" into a precise "send me exactly what I missed."
Why you do not reload everything
The other common mistake is the opposite extreme: on reconnect, throw away local state and reload the full history. This is correct in the sense that the client ends up current, but it is wasteful and it scales terribly. A user in a busy channel with tens of thousands of messages should not re-download all of them because their phone blinked. That is a huge, slow payload for a gap that might be three messages, and it punishes exactly the users on the worst networks, who reconnect the most.
Cursor-based catch-up sends only the delta. A three-message gap is three messages, not the whole channel. This is the same "send only what changed" discipline that runs through sending deltas instead of full state, applied to reconnection. The full reload is a blunt instrument; the cursor is a scalpel.
The durable log is what makes it possible
None of this works without a durable log on the server. If the server only holds recent messages in memory, or drops them once delivered, there is nothing to replay when a client asks for the gap. The ordered history has to live somewhere that survives, long enough to cover any realistic disconnect, so the server can answer "everything after 40" even if 40 happened a while ago. This is why the live channel and the stored history are two different systems, a separation I make the full case for in separating chat history from live delivery. The socket delivers; the log remembers.
Retention is a real decision here. The log needs to cover the longest gap you want to resume seamlessly. Beyond that window, you fall back to a bounded reload or a "load older" path, which is fine, because past a certain age the user is scrolling history rather than catching up on a brief outage.
Let the platform hold the log
Building a durable, ordered, replayable message log with cursor-based resume is real infrastructure, and it is the same infrastructure whether your app is chat, collaboration, or notifications. I do not want to rebuild it per app, so I lean on the real-time layer of AltoHost to hold the ordered log and serve catch-up from a cursor, and I spend my effort on the product instead. If you are deciding whether to build that yourself, the tradeoff is the same one I worked through in build vs buy real-time infrastructure.
The test is simple: disconnect a client, let real activity happen, reconnect, and check for a hole. If there is a gap, you resumed live without a cursor. If the client re-downloads everything, you reloaded instead of catching up. Done right, the client comes back and slots in exactly the messages it missed, no more and no less, and the user never knows the connection dropped at all.