Skip to content

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 Realtime client from @lepsto/sdk/realtime/server. The examples use its messages.history method; the generated public client uses history.get instead.
  • For browser recovery, a token containing the channel’s subscribe and history operations.

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" }
  ]
}
  • epoch is the channel’s current epoch, or null when the channel has no history yet.
  • entries are in publish order, oldest first.
  • Each entry carries the message id, the publish timestamp ts in milliseconds, and its offset.
  • An entry normally carries its payload inline as data. Payloads larger than 262144 bytes are stored separately and the entry carries a ref instead — bucket_key, size and an optional content_type — with no data field.

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:

  1. Publish a backend message to chat:room-1. Confirm offset and epoch are present; if absent, check the namespace’s history setting.
  2. Call realtime.messages.history('chat:room-1', { lastN: 1 }) and find that message with recovered: true.
  3. Subscribe a tab, disconnect its network briefly and publish another message. Restore the network before retention expires. Confirm the missed message arrives and onSubscribed reports recovered: 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

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select