Skip to content

Real-time

Changefeeds

Changefeeds are how you ask SurrealDB for a history of writes to a table (or database) so you can replay them elsewhere: search indexes, warehouses, cache warmers, or any system that needs to catch up after downtime.

You define how long changes are retained when you set up the feed by adding a duration like 3d or 100h. Changes are then read with the SHOW statement.

Before SHOW CHANGES returns anything useful, the table or database must opt in with a CHANGEFEED definition (duration, scope). Changefeeds are usually defined using a DEFINE TABLE statement, but can be defined on the entire database if desired via DEFINE DATABASE.

A typical flow is:

  1. Note when you last processed changes (a datetime) or where you stopped (a versionstamp).

  2. Ask for everything since that marker, optionally with a limit so you page through large backlogs.

The SHOW statement reference spells out the full syntax. In practice you are choosing: “give me the next batch of mutations after this cursor.”

-- Define the changefeed and its duration
DEFINE TABLE reading CHANGEFEED 3d;

-- Create some records in the reading table
CREATE reading SET story = "Once upon a time";
CREATE reading SET story = "there was a database";

-- Replay changes to the reading table since a date
SHOW CHANGES FOR TABLE reading SINCE d"2025-09-07T01:23:52Z" LIMIT 10;
-- Replay changes to the reading table since a versionstamp
SHOW CHANGES FOR TABLE reading SINCE 1 LIMIT 10;

If the datetime is after the changefeed was created and lines up with your retention window, you get a list of entries, each with changes and a versionstamp (where you are in the log). How each mutation is encoded depends on your DEFINE TABLE ... CHANGEFEED options; when differences are stored (INCLUDE ORIGINAL), an update to an existing record carries a reverse diff from the current state to the state immediately before that write. The SHOW reference covers this in full. The following shape is representative:

Output
[
	{
		changes: [
			{
				define_table: {
					changefeed: {
						expiry: 3d,
						original: false
					},
					drop: false,
					id: 0,
					kind: {
						kind: 'ANY'
					},
					name: 'reading',
					permissions: {
						create: false,
						delete: false,
						select: false,
						update: false
					},
					schemafull: false
				}
			}
		],
		versionstamp: 116372747574247424
	},
	{
		changes: [
			{
				update: {
					id: reading:1up24usipwi7wjs4nbgk,
					story: 'Once upon a time'
				}
			}
		],
		versionstamp: 116372747574247425
	},
	{
		changes: [
			{
				update: {
					id: reading:6q3845opi3nikxr7jvgg,
					story: 'there was a database'
				}
			}
		],
		versionstamp: 116372747574312960
	}
]

A change feed can keep a second SurrealDB instance up to date with the first. Each round reads the changes from the source with SHOW CHANGES, and applies them to the target in one transaction, together with the versionstamp to continue from next time. An update entry holds the whole record after the write, so UPSERT with that record recreates it on the target, and a delete entry holds the ID of the removed record.

Keeping the versionstamp in a record on the target, here sync_state:reading, means it changes in the same transaction as the data. If a round fails partway, neither moves, and the next round starts from the same place.

A program has to run the rounds, because each one connects to two instances, and SINCE only accepts a literal value. The program reads the versionstamp from the target and writes it into the text of the SHOW CHANGES query, rather than passing it as a parameter.

-- 1. On the target: where to continue from
sync_state:reading.since ?? 0;

-- 2. On the source: the changes from that versionstamp on.
-- SINCE includes the entry with this versionstamp itself.
SHOW CHANGES FOR TABLE reading SINCE 117368726935830530 LIMIT 1000;

-- 3. On the target: apply the changes and store the next
-- versionstamp, which is the last one returned plus 1
BEGIN;
FOR $change IN $changes {
	IF $change.update {
		UPSERT $change.update.id CONTENT $change.update;
	} ELSE IF $change.delete {
		DELETE $change.delete.id;
	};
};
UPSERT sync_state:reading SET since = $next;
COMMIT;

Entries that change the table definition, such as define_table, are not applied, so define the table on the target separately.

The source keeps changes only for the duration given in its CHANGEFEED clause. If the program stops for longer than that, the changes it missed are gone, and the target has to be copied again with an export before the program starts again.

Was this page helpful?