Replay what a client missed
Read retained channel messages and recover gaps after a reconnect.
Enable history on a namespace to let clients replay messages after reconnecting. History is off by default, and recovery works only while the messages a client missed are still retained.
This chat example uses namespace chat and channel chat:room-1. Replace them with your own names.
Before you start
- A registered namespace; the Realtime quickstart shows how to create one.
- Owner or Admin access to change its policy.
- A backend product public key and the
Realtimeclient from@lepsto/sdk/realtime/server. The examples use itsmessages.historymethod; the generated public client useshistory.getinstead. - For browser recovery, a token containing the channel’s
subscribeandhistoryoperations.
Step 1 — Choose what the namespace keeps
Choose a retention mode for all channels in the namespace:
Mode | What is kept |
|---|---|
none | Nothing. This is the default. |
last-message | Only the most recent message on each channel. |
window | Every message published within a retention window. |
For window, set historyWindowSeconds to a positive integer no greater than 2592000 seconds (30 days). Choose a window long enough for the disconnections you need to recover from. Older messages are no longer available through live history.
In Realtime → Namespaces, edit the namespace’s History setting. Selecting Window reveals History window (seconds). Agents can use realtime_namespace_update; public HTTP routes cannot change namespace policy.
Step 2 — Publish, and keep the cursor
Publish from your backend and check the response for a history cursor:
const result = await realtime.messages.publish('chat:room-1', { text: 'hello' })
// { channel: 'chat:room-1', published: true, offset: '1754130000123-0', epoch: 'b8d2…' }Save offset and epoch together. offset locates the message within the channel’s current history, and epoch identifies that history’s current run. Neither field is returned when history is disabled.
Step 3 — Read the history back
Read the retained message using your backend’s product public key and the server SDK. It calls the history route:
const page = await realtime.messages.history('chat:room-1', { lastN: 50 })Agents can read history with realtime_history_get.
Select either a window or a complete cursor. The HTTP and MCP parameter names are:
Ask | Parameters | Returns |
|---|---|---|
Window | last_n, an integer 1–1000 | The last N entries |
Window | last_ms, a positive integer | Entries newer than now minus that many milliseconds |
Cursor | offset and epoch, both required together | Entries strictly after that offset |
A cursor requires both offset and epoch. A successful read has this shape:
{
"channel": "chat:room-1",
"epoch": "b8d2…",
"recovered": true,
"entries": [
{ "id": "0f0a…", "data": { "text": "hi" }, "ts": 1754130000123, "offset": "1754130000123-0" }
]
}epochis the channel’s current epoch, ornullwhen the channel has no history yet.entriesare in publish order, oldest first.- Each entry carries the message
id, the publish timestamptsin milliseconds, and itsoffset. - An entry normally carries its payload inline as
data. Payloads larger than 262144 bytes are stored separately and the entry carries arefinstead —bucket_key,sizeand an optionalcontent_type— with nodatafield.
A 404 means the namespace is not registered. A 503 means history is unavailable; retry the read.
Step 4 — Let a reconnecting tab catch up
When subscribing, request the initial history window you need. The browser client then tracks the last message’s offset and epoch on each channel and uses that cursor when it reconnects.
Handle failed recovery in onSubscribed. Replace reloadFromYourBackend with a function that reads your application’s current state:
socket.subscribe('chat:room-1', onMessage, {
history: { lastN: 50 },
onSubscribed: ({ recovered }) => {
if (recovered === false) reloadFromYourBackend()
},
})The callback runs for the first subscription and after every reconnect. It is where your application learns whether the missing messages were recovered.
Verify it worked
Test both the read and a real interruption:
- Publish a backend message to
chat:room-1. Confirmoffsetandepochare present; if absent, check the namespace’s history setting. - Call
realtime.messages.history('chat:room-1', { lastN: 1 })and find that message withrecovered: true. - Subscribe a tab, disconnect its network briefly and publish another message. Restore the network before retention expires. Confirm the missed message arrives and
onSubscribedreportsrecovered: true.
A successful history read alone does not check the browser’s reconnect behavior; the interruption test checks both.
When recovery is refused
A cursor read or subscription reports recovered: false when it cannot recover the complete gap:
- The epoch has changed, so the old offset no longer identifies a position in the current history.
- Messages needed to fill the gap have expired from the retention window.
Both cases return no entries. Replace your stream-derived local state with a fresh read from your database or API, then continue processing live messages.
Delivery and ordering
- Within a channel, retention and replay follow publish order, and offsets increase.
- Delivery is at least once within the retention window. Reconnects, overlapping live delivery and replay, or repeated reads can produce duplicates.
- Deduplicate using the message
id, which stays unchanged across live delivery and replay. Keep processed IDs for your recovery window. - Live history cannot return messages that have expired, whether you request a cursor or a window.
Keep history for longer
For longer retention, set archive to { days: N }, with N from 1 to 3650; off disables it. History must be enabled. Archived entries remain readable after leaving the live window, until the archive retention period expires and they are deleted.
In Realtime → Namespaces, the Archive controls become available when History is not none. To read or export archived entries, open Realtime → Archive, enter the channel, then select Load or Export. Export writes the channel to an NDJSON file in the product bucket. The MCP tools are realtime_archive_get, realtime_archive_export and realtime_archive_export_status.
Next steps
- Connect a browser tab: how the socket tracks the cursor and resubscribes for you.
- Call Realtime from your backend: the
historyquery shapes and what an entry carries. - Show who is on a channel: the other thing a namespace can keep for you.
- Look up a limit or an error: retention ceilings,
last_nbounds and payload sizes.