Skip to content

Realtime

Replay what a client missed

Retain messages on a channel so a reconnecting tab catches up from where it left off.

History lets a channel keep the messages published to it so a client can read them back — on first subscribe, or after a reconnect that made it miss something. It is off by default and turned on per namespace.

Turn history on

History is a namespace setting, so it applies to every channel under that namespace. It has three modes:

ModeWhat is kept
noneNothing. This is the default.
last-messageOnly the most recent message on each channel.
windowEvery message published within a retention window.

With window you also set historyWindowSeconds — the retention window in seconds. It is required in that mode, must be positive, and may not exceed 2592000 seconds (30 days). Messages older than the window stop being readable.

Namespaces are configured from the platform, not through the public routes — over MCP, realtime_namespace_update.

What publishing returns

When the namespace retains history, a publish is acknowledged with two extra fields:

{ "channel": "chat:room-1", "published": true, "offset": "1754130000123-0", "epoch": "b8d2…" }

offset is the message’s position on the channel and epoch identifies the current run of that channel’s history. Together they form a cursor. When the namespace keeps no history, both fields are absent.

Messages published directly by a browser client are ephemeral: they are fanned out to subscribers but never retained, so they carry no offset or epoch and cannot be replayed.

Read history

Your backend reads history on the history route listed in what your API key reaches. With the server SDK:

const page = await realtime.messages.history('chat:room-1', { lastN: 50 })

Over MCP, realtime_history_get.

Ask for either a window or a cursor, never both, and never neither:

AskParametersReturns
Windowlast_n, an integer 1–1000The last N entries
Windowlast_ms, a positive integerEntries newer than now minus that many milliseconds
Cursoroffset and epoch, both required togetherEntries strictly after that offset

Sending only one half of a cursor is rejected. The response looks like this:

{
  "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.

Reading a channel whose namespace is not registered returns 404. When the history backend cannot be reached the read returns 503; retry it.

Recovery on reconnect

The browser client tracks the offset and epoch of the last message it received on each channel. When the connection drops and comes back, it resubscribes with that cursor instead of the original history request, so the client gets exactly what it missed.

The subscribe acknowledgement tells you whether that worked:

client.subscribe('chat:room-1', onMessage, {
  history: { lastN: 50 },
  onSubscribed: ({ recovered }) => {
    if (recovered === false) reloadFromYourBackend()
  },
})

onSubscribed fires on the initial subscribe and again after every reconnect.

What a refused recovery means

recovered: false — on the subscribe acknowledgement or on a cursor read — means the gap cannot be filled. It happens for two reasons:

  • The channel’s epoch no longer matches the one in your cursor. The channel’s history has started a new run and the old offsets mean nothing in it.
  • The messages between your cursor and the current head have aged out of the retention window, so part of the gap is gone for good.

In both cases no entries are returned. Treat it as a resync signal: discard whatever local state you built from the stream and re-read the current state from your own source of truth — your database, your API — then carry on with live messages.

Do not try a second recovery with the same cursor. It will be refused again.

Delivery and ordering

  • Messages on one channel are retained and replayed in publish order. Offsets on a channel only increase.
  • Delivery is at least once within the retention window. A reconnect, a replay that overlaps live delivery, or a repeated history read can hand your client the same message more than once.
  • Deduplicate by message id. Every message carries an id that stays the same across live delivery and every replay. Keep the ids you have already processed for as long as your recovery window and drop repeats.
  • Nothing is guaranteed outside the retention window. Once a message ages out, neither a cursor read nor a window read will return it.

Keep history for longer

A namespace can also be given an archive setting: off, or a number of days from 1 to 3650. Archiving requires history to be on, and it keeps entries that have already aged out of the live retention window readable for the configured number of days. Entries older than that budget are deleted.

Archived history can also be exported as a file for offline use. Both the archive setting and exports are handled from the platform — over MCP, realtime_archive_get to read it, realtime_archive_export to start an export and realtime_archive_export_status to poll it.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect