Replication

Keep read replicas of a server, promote one when the primary is gone, and back up every write so you can restore to any moment.

Start a replica

Give the primary a replication token, then start a second fenec-server on its own file pointed at the primary. The replica serves reads; when the primary is gone, promote it to take writes.

# the primary: replicas are fed over its HTTP listener
fenec-server --file primary.fenec --http 0.0.0.0:8080 --http-token "$HTTP" \
         --replication-token "$REPL" --sync always

# a replica, over a file of its own
fenec-server --file replica.fenec --http 0.0.0.0:8081 --http-token "$HTTP" \
         --replication-token "$REPL" --replica-of http://primary:8080

# the primary is gone: the replica takes writes from here on
curl -X POST replica:8081/_replication/promote -H "Authorization: Bearer $REPL"
# {"promoted":true,"seq":41873,"history":"5f0e2c9a81b44d17"}
  • Every read works on a replica, subscriptions included.
  • Writes are refused until it is promoted, by hand.
  • A failover can lose only what the primary's --sync had not yet put on disk.
What a replica is and answers

A primary sends its replicas the writes that have reached its disk, and each applies them to a file of its own and serves reads from it.

A replica is a whole fenec-server: every read works on it, subscriptions included, from its own copy in memory, and a write gets 403. GET /_replication says which role a server has, so a client or a load balancer can tell a replica from the primary.

What is sent

A replica is sent the primary's writes in order and applies each one to its own file. Only writes already on the primary's disk are sent.

  • A restarted replica asks only for what it missed.
  • A replica far behind (past 64 MiB of writes) is sent a whole copy of the database instead.
  • --sync off is refused on a server with a replication token.
How records are sent and caught up

A fenecdb file is already a log: an image, then every write appended as a record. A replica is sent those records, whole and in order, and applies each the way the write path made it — the document, and the upkeep of every index on it — and appends it to its own file. Its file therefore ends as the primary's does, and a replica stopped and started again reopens where it was and asks only for what came after. The primary keeps the last 64 MiB of writes for this (--replication-buffer); a replica further behind than that is sent an image of the whole database instead, and follows write by write from there.

Only writes on the primary's disk are sent. A write enters the feed when it is appended and leaves it once an fsync has covered it, so a primary that crashes comes back holding every write any replica was sent, and a replica never has to be told to forget one. The price is latency, set by the primary's --sync: under always a write goes out right after its own fsync, under 250 up to 250 ms later. Under off nothing is on disk before shutdown, so a server with a replication token refuses to start with it.

Positions and histories

A replica's position is a change number that means the same on the primary and on every replica. After a promotion the history forks, and a replica is only continued from a point the new primary also passed through; otherwise it is sent a fresh copy.

A replica that…is sent
reconnects to a primary that restarted, even after a crashthe writes it missed
was stopped and started again over its filethe writes it missed
is pointed at a promoted replica, holding nothing past the forkthe writes it missed, and the new history
is pointed at a promoted replica, holding writes past the fork — the old primary, sayan image
is further behind than the primary keeps writes foran image
starts emptythe writes from the first, or an image
Positions, histories and forks

Every write moves the change counter by one, on the primary and on each replica alike, so a replica's position is its own counter and a subscriber's cursor means the same change on either. A block — the writes of one statement, a /batch — is one record numbered by its last write, and is sent and applied whole: a replica never stands inside one. A replica of a replica is fed exactly as its primary was.

Numbers alone stop being enough once a history forks. A replica promoted after its primary died writes its own change 1 001; the old primary, or a second replica that had got further along, may hold a different 1 001. Streaming the new primary's 1 002 on top of that would go unnoticed. So each fork gets a fresh id and remembers the change where it left its parent, as PostgreSQL numbers its timelines, and a replica is continued only from a position the primary's own history passed through, as the table above shows.

The history lives in the file (record kind 8), and the first time a primary serves replicas it draws an id of its own — the root history is every database's, so it names none.

Reading your own writes

A replica can be a moment behind, so a read sent right after a write may miss it. Send the number a write's answer carries in Fenec-Seq as Fenec-After, and the replica waits until it has that write.

curl -i -X POST http://primary:8080/query -d '{"query":"put carts {user: 7}"}'
# Fenec-Seq: 1042

curl -H 'Fenec-After: 1042' -X POST http://replica:8080/query \
  -d '{"query":"get carts where user = 7"}'
  • It waits 5 seconds unless Fenec-Wait says otherwise (in milliseconds, 30 s at most), then answers 504.
  • A subscription and /_changes wait too, before their head: a stream sent with the header is seeded with the write, and a change-stream page's Fenec-Seq is never behind it.
  • Across a failover, send Fenec-History too: the answer names the history its change is of, which a promotion forks. A replica left on the old branch holds a change of that number of its own, and answers no read sent after the new one.
  • Its token comes first: a request the server refuses (401, or 403 for another tenant's token) is answered at once, not after the wait, and learns nothing of where the replica stands.
  • A read marked Fenec-Read: replica names where it read: the answer carries the copy's Fenec-Seq and Fenec-History, so a read sent after it never reads older (Read replicas). A read nobody marked names nothing.
  • The Python client keeps the number for you: client.seq, sent with query(..., after=...).
How Fenec-Seq and Fenec-After work

A replica is behind its primary by the time a write takes to reach it — 0.04 ms at the median under --sync always, up to the sync interval otherwise — so a client that writes on the primary and reads on a replica can read from before its own write. Over HTTP a write's answer names the change it left the database at, Fenec-Seq; a request sent to a replica with Fenec-After: <that number> is served once the replica holds it, and answered 504 with where the replica stands if it does not within the wait — never from before the write.

A change number names a write within one history. A promotion forks the history where the replica promoted stood: a write the old primary answered and the replica never got is lost, the new primary's next write takes its number, and a replica still following the old primary holds a change of that number — the lost write. So an answer of a database that has had a replica also names its history, Fenec-History: <term>-<id>, the term one more at each promotion; a read sent with it beside Fenec-After is served once the replica holds that change of that history — a replica on the old branch waits until it follows the new primary and takes its image. A primary asked for a position its history does not hold — the write lost — answers 504 at once, with its own seq and history, which a client takes as where it stands from then — over the position it sent that read after alone, and never an earlier term of the same lineage, which only a fenced old primary names. A history's lineage is the top half of its id: every promotion and restore keeps it, and a database made anew — a tenant deleted and made again under its name, starting again at term 1 — has another, whose position a client takes, its term lower or not.

The wait is 5 seconds unless Fenec-Wait gives it in milliseconds, 30 seconds at most, and a replica waits on the same Condvar its subscriptions do, so nothing polls. On a tenant node a wait takes one of the tenant's streams, but only once it must wait: a read after a write the node holds already takes none, as a tenant whose streams its own subscriptions fill still reads its writes. Writing the header costs the server nothing measurable; a client that parses every header, as Python's http.client does, reads one more.

Promotion

Promote a replica by hand: POST /_replication/promote on its HTTP listener, or restart it over its file with --promote in place of --replica-of. Keeping the old primary from taking more writes is up to you.

What promotion forks, and the old primary's part

Either way forks the history where the replica stands and turns writes on. A replica's file does not open any other way — started with neither flag, fenec-server refuses and says which to give — because a replica taking writes without a fork would continue its primary's history with writes the primary never made.

Making sure the old primary takes no more writes is the operator's part, as it is with PostgreSQL: nothing here fences it. Brought back, it rejoins under the new primary with --replica-of, and is sent an image if it wrote past the fork.

What a failover loses

With --sync always on the primary, no acknowledged write was lost in ten failovers. With the default --sync 250, a failover loses up to the last 250 ms of writes.

primary's --syncacknowledged writes/slost per failover
always2840 in all ten
25053 5659 700–10 788: the last ≤ 201 ms
Method, and what a failover loses

How it was measured: a primary killed with SIGKILL while one client writes to it as fast as it answers, the replica promoted half a second later, ten times per policy; counted are the writes the client was told succeeded that the promoted replica does not hold (make replica-bench, macOS, loopback).

Under 250 the loss is the policy's own: those writes were answered before any fsync and were not on the primary's disk either. Under always a write is answered after its fsync and once the stream to each replica has written it into its socket, which the kernel delivers after the process that wrote it is killed. Sent after the answer, as it was before, a write answered in the fraction of a millisecond before it was sent was lost: none of these ten failovers caught one, while a node of the SaaS example killed under eight writers lost one in 3 of 60 runs, and none in 120 since. A stream that has not sent a write within a second is not waited for until it catches up again, so a stalled replica costs the writes one wait. A machine that dies takes its sockets with it: this holds for a process killed, not for a power loss. No failover left the replica holding a write the client had not been answered for.

What it costs

A write is readable on a replica 0.04 ms after the primary answered it under --sync always, and within the sync interval otherwise. A replica keeps up with a bulk load.

primary's --syncp50p95p99max
always, writes back to back0.04 ms0.06 ms0.09 ms2.77 ms
250, 250 writes a second148 ms262 ms273 ms282 ms
Method, and the lag and bulk-load figures

How it was measured: primary and replica in one process over loopback, each over its own file; lag is from a write's answer on the primary to the write being readable on the replica, over 1 000 writes.

A replica keeps up with a bulk load: 100 000 documents of 128 dimensions with an HNSW index, which the primary wrote in 23.4 s in statements of 1 000, were applied from the stream in 19.7 s — 5 088 writes a second, the graph built as they arrive. A new replica of that database sent it as an image — 58.3 MB, the graph in it — answered from it 0.17 s after it connected.

The primary keeps the writes in memory up to its buffer, and copies the image it sends; a replica holds the database in memory as a primary does. The replica's HNSW graph is built from the same vectors in the same order, but not in the same batches, so an approximate near can rank a close call the other way on the two; everything else answers identically.

Backups and restoring to a moment

An archive is the replication stream written to a directory: base images and every write after them, timestamped. From it, fenec restore rebuilds the database as it stood at any moment or change.

# the running database, taken whole: a file that opens as a database of its own
fenec backup http://primary:8080 nightly.fenec --token "$REPL"

# every write that reaches the primary's disk, until interrupted; an image of
# its own every hour, and what no restore to the last week needs let go of
fenec archive http://primary:8080 /backups/archive --token "$REPL" --image-every 1h --keep 7d

# what the archive can restore, read as a restore would; exits 1 if it cannot
fenec verify /backups/archive

# the database as it stood just before the mistake
fenec restore /backups/archive before.fenec --to 2026-09-22T10:14:59Z
fenec restore /backups/archive before.fenec --to-change 41873
  • --to stops at a moment; --to-change after a change number.
  • --keep (or fenec prune) deletes what no restore within that window needs.
  • fenec verify says from when to when the archive can restore.
How a restore to a moment works

The same stream written to a directory instead of applied is an archive: base images, and every write after them with the time the primary appended it. The fenec command line writes and reads one.

A restore takes the latest image at or before the point asked for and appends the archived writes after it up to there — a fenecdb file is an image and the writes after it, so that is the whole restore — then opens the result to check it, forks its history and checkpoints it. --to keeps every write the primary appended at or before that moment and stops at the first one after it; --to-change stops after that change. A statement writes each document as a change of its own, so a del of ten documents is ten changes.

The archive is fed as a replica is: only writes that were on the primary's disk, so none a crash took back, and a compact on the primary loses it nothing — the writes were archived before a compact folded them into an image. An archiver that falls further behind than the primary keeps writes for takes a new image and goes on from there; the archive then holds no write between the two images, and a restore to a point in that gap says so rather than inventing one. Restoring the 100 000 × 128 load above from an image and its 100 000 writes took 19.9 s, most of it building the graph for them, and the file then opened in 63 ms; to the load's middle, 7.8 s. A base image a day keeps a restore to a day of writes.

image-<change>-<time>.fenec   a base image: the database at that change
writes-<first change>.log      per record, the time it was appended and the record
history                        the history the archive is on

An archive takes images of its own: every --image-every (an hour unless given), once a write came after the last one, it opens its last image and the writes after it and adds an image of the database there — the primary is asked for nothing — named by the time of its last write, and the segment being written ends so that the writes before the image are in segments of their own. A restore then replays at most that much. With --keep, what no restore within that window needs goes after each image: the images before the newest one taken by the window's start, which a restore to that moment starts from, and the segments whose writes all came before the oldest image kept — never the one being written. fenec prune <dir> --keep 7d does the same once.

fenec verify <dir> reads the archive as a restore would: every image opens at the change its name says, the segments run on from the oldest image with no change missing, only the last segment may end in a record cut short, and a restore to the end opens. It prints from when to when the archive can restore, names any gap — changes in no segment between two images, where a restore to the end passes but a restore to a moment inside cannot — and exits 1 when no restore can be made.

Archiving every tenant of a node

A node of tenants (fenec-server --dir) is archived by one process over one connection: an archive a tenant under a root, each the archive above, which fenec restore, verify and prune read as they read any.

# every tenant of the node into /backups/n1/<tenant>/, until interrupted
fenec archive --tenants http://n1:8081 /backups/n1 --token "$REPL" --image-every 1h --keep 7d

# every tenant's archive read as a restore would; exits 1 if any cannot
fenec verify --tenants /backups/n1

# the node's directory as it stood at a moment: <tenant>.fenec for each tenant it held then
fenec restore --tenants /backups/n1 /data/n1 --to 2026-09-22T10:14:59Z
fenec-server --dir /data/n1 --replication-token "$REPL" ...
  • One connection and one thread on the node, however many tenants it holds, and no tenant held open: an idle tenant stays closed. One tenant's image or a slow archiver holds up no other tenant's writes.
  • A tenant made on the node is archived from its first image; one deleted or moved to another node is no longer followed, and its archive is kept, noted gone.
  • A tenant restored to an earlier moment, or made again under its name, begins a new archive; the one before is kept beside it, and answers for the time before.
  • --key-file, --image-every and --keep apply to each tenant's archive.
  • Under load the archive trails the node. Under --sync always a write is answered once the stream has sent it, as a replica's stream would, but only while the stream keeps up: while it is busy — a tenant's image, a closed tenant opened, a tenant's lock waited for, an archiver reading slower than the node writes — writes are answered without it. Lose the node's disk then and the archive lacks them; where the node survives they reach the archive later, at worst inside an image. Under --sync 250 it trails anyway, as a replica does.
How one stream archives every tenant

A tenant's own stream (/t/<tenant>/_replication) is a connection and a thread on the node for as long as it runs, one of --max-connections, and it holds its tenant open the while. Archived that way, a node of a thousand tenants holds a thousand of each and every tenant open for good — and at the default of 100 connections it cannot be archived at all. fenec archive --tenants asks the node for POST /_replication/tenants instead, with the replication token: every tenant's writes over one stream, each message led by its tenant's name. Its body says where each archive under the root stands; the node says hello to every tenant it holds — where the archive goes on from, or an image where its place does not lead to the tenant's writes — and that every tenant the archive lists and the node does not hold is gone.

The stream sends a tenant's writes from its feed while it is open, and nothing while it is closed, whose file does not change. A tenant closes only once the stream has sent what its disk holds — closed with writes unsent, its feed would go with it and they would come again as an image — and is looked at again at the next pass, never waited for. Under --sync always a write waits for the stream as for a replica while the stream keeps up; whatever can keep it busy — reading or opening a closed tenant, waiting for a tenant's lock, which its longest write holds (a /batch, a compact under the lock), an image's snapshot and its sending, an archiver reading slower than the stream writes — it does held up, and no write waits for it then. One stream for every tenant held each tenant's next write for a second while it opened another one; each tenant's own stream holds up its own tenant alone. An image waits for its tenant's disk without holding up the stream — and is taken again, from the tenant as it is then, where the tenant closed and opened again meanwhile — and the node's syncer and a closing tenant wait for no stream at all, since no client's answer waits on them.

So the archive of a node is synchronous only while its stream keeps up, and under load it is not: a write answered while the stream was held up, or behind after an image, is on the node's disk and not yet in the archive. It is sent once the stream comes back to its tenant, or, where the stream fell behind the tenant's feed, as part of an image; if the node's disk is lost before that, the archive does not have it. A replica's own stream (/t/<tenant>/_replication) keeps a write's answer waiting for it, up to a second.

Which tenants have writes is told, not polled: a tenant's feed tells the stream as its writes reach the disk — with no stream connected a sync pays two loads, the same 13 ns a durable advance either way — and a tenant made, installed, deleted, promoted or made to follow tells it too; one released from under the stream is looked at again until it is gone or back. Where a closed tenant stands is known where it closed in the node's process, and read from its file otherwise — the first stream after the node starts: 1 001 tenants in 0.29–0.33 s, where an open, which fsyncs the file first, was 6.3 ms a tenant. A closed tenant the stream is told of is read again rather than taken where it closed, since a holder of its database — the @ttl sweeper — may have written it since; one with a sync log beside it, after the machine went down, is opened.

A tenant's archive is <root>/<tenant>/, written as fenec archive writes one; its segment file is open only while the tenant writes, so the archiver holds a descriptor a writing tenant, not a tenant. When the node says a tenant is gone — deleted, or moved to another node, whose own archiver takes it up from its image on the same history — the archive is kept as it stands and <root>/<tenant>/presence notes the time; a tenant that comes back is followed again and noted back, the time it was away a gap. The times are the node's clock, as its writes' are: gone is when the node deleted the tenant, though the stream was busy or the archiver away when it did — or, where the node does not know (it restarted since), when the stream saw it go — and back is when the node said hello to it again. A tenant deleted and made again while the stream was busy is noted gone at the delete before the new copy's hello; one deleted and made again while the archiver was away is heard of as the new copy alone, and the line it moves aside (below) is noted gone by that hello at the latest, so a moment between the delete and then restores the old copy. A tenant whose copy on the node no longer leads from its archive's place, at or before it — restored there to an earlier moment, or another tenant made under the name — would land its writes among changes of the same numbers: the line so far is moved aside to <root>/<tenant>.<history>/ and a new one begins. (fenec archive of one database refuses such a primary instead, and leaves its archive as it was.) A promotion's new history reaches the archive before the writes after it, so a promoted tenant's archive goes on without an image. <root>/archiving.lock keeps a second archiver off a root.

fenec restore --tenants <root> <dir> writes <dir>/<tenant>.fenec for each tenant the node held at --to — from the line of its history that began last by then, so a line moved aside answers for the time before the next began — or at each archive's end. A tenant the node said was gone then, deleted or living on another node, is passed over, and so is one archived only after it; one whose moment --keep let go of is refused, and the restore exits 1 — each line knows when it began, which the prune that lets its first image go writes down first (began). --to-change names one database's change, and is refused. fenec verify --tenants and fenec prune --tenants run on every line, those moved aside too, and verify names a gap an image alone ends.

Measured with make node-archive-bench: a node of 1 000 tenants of ten rows each and one of 64 MB, closed when idle, ten of them written ten times a second from before the archiver connected and for 20 s after, under --sync always, in turns, on this M1:

 no archivera stream eachthe node's stream
node: threads151 01616
node: descriptors274 01129
node: tenants open101 00110
node: CPU, of a core4.7–5.1%7.0–8.0%3.9–6.0%
archiver: threads—+1 001+1
archiver: descriptors—2 02121
archiver: CPU, of a core, the writers' 1.2–1.3% in it—3.4–4.2%2.1–2.9%
first images of the 1 001—11.2–15.5 s14.8–15.2 s
writes as they were taken, p50 / p99 / max—28–33 / 176–600 / 436–1 113 ms14.5–14.7 / 32–35 / 43–116 ms
writes after, p50 / p9911.8–12.1 / 22.7–26.4 ms13.0–14.2 / 28.7–55.4 ms12.4–13.8 / 25.6–33.8 ms
node restarted: memory, tenants open—121–157 MB, 1 0012.2–4.6 MB, 0
--sync 250: the syncer's pass, p50 / max30.7–32.6 / 45–63 ms65–86 / 84–353 ms52.3–52.7 / 91–168 ms
--sync 250: a tenant's close, p50 / max3.8–4.0 / 12–25 ms— (none closes)4.0 / 15–33 ms

The archiver's process runs the bench's writers as well. A write's time under --sync always is the Mac's F_FULLFSYNC; a write waits for the node's stream as for a replica, and the stream sends within the fsync's noise. While the first images were taken no write waited for the node's stream; held up as a replica's stream is, it held every written tenant's next write for a second whenever it opened another tenant. A stream each held the writes up as images went out over a thousand sockets at once, the build before alike (p99 86–351 ms, at most 110–994). The node's stream takes a new archive's first images one tenant at a time, each tenant opened — an fsync — and its image written: about 15 s for the thousand and one, once, the 64 MB one among them. After the node restarted, the archiver connecting again, the stream said hello to all 1 001 tenants in 0.29–0.33 s, each read from its file rather than opened; a thousand streams opened every tenant again and held it.

Under --sync 250 (node_archive_passes: eight tenants written every 5 ms, eight more opened, written and closed in turn, one of 32 MB closed) the syncer's pass took 52 ms at the median beside the node's stream against 31–33 with none: the archiver writes the same disk, and the passes are its fsyncs and the node's together. Before the syncer and a close stopped waiting for streams, a stream busy with one tenant held a pass for a second a tenant, 5.06 s over five, and a close under its lock for 1.01 s. Against the build before, in turns: with no archiver the syncer's pass took 31.0–31.4 ms at the median and a close 3.6–3.9 ms, and with a stream each the pass 67–73 ms, as here; the writes with no archiver or a stream each as in the table.

In object storage

To keep an archive in S3, R2 or GCS, run a sync tool beside the archiver; fenecdb has no object-storage client of its own.

rclone sync /backups/archive r2:backups/archive      # every few minutes
aws s3 sync /backups/archive s3://backups/archive --delete

# to restore: bring it back, check it, restore it
rclone sync r2:backups/archive ./archive && fenec verify ./archive
fenec restore ./archive before.fenec --to 2026-09-22T10:14:59Z
How a bucket copy stays consistent

fenecdb speaks no TLS, and S3, R2 and GCS speak only HTTPS, so an archive goes there the way any directory does: a sync tool beside the archiver.

Every file but the segment being written is written once: a segment is closed and fsynced before the next begins, and an image is written beside and renamed into place. A sync tool lists the directory and then copies each file as it stands, so the copy it leaves is the archive up to a moment, its last segment at most cut in a record's middle — which verify reports and a restore passes over. A test copies an archive a file at a time while writes and images go on, and every copy verifies and restores to a prefix of what the primary wrote. --delete, or rclone sync's own, carries a prune over to the copy; without it the copy keeps everything.

Sealed archives and backups

Give a key file and every archive file and backup is encrypted and tamper-checked. Keep the key apart from the archive: without it nothing can be restored.

fenec key /secrets/archive.key          # 64 hex digits, readable by its owner alone
fenec archive http://primary:8080 /backups/archive --key-file /secrets/archive.key
fenec verify /backups/archive --key-file /secrets/archive.key
fenec restore /backups/archive before.fenec --to 2026-09-22T10:14:59Z \
  --key-file /secrets/archive.key

fenec backup http://primary:8080 today.sealed --key-file /secrets/archive.key
fenec restore today.sealed today.fenec --key-file /secrets/archive.key
How archives and backups are sealed

A copy in a bucket or on another disk is out of reach of the machine's disk encryption. With a key every file of an archive — its images, its segments, its history — and a backup file are sealed: encrypted and authenticated with ChaCha20-Poly1305 (RFC 8439), a random nonce a frame, each frame's place in the file and whether it is the last under its tag. A byte changed, a frame moved or dropped, a file cut short where it was sealed whole, or the wrong key, and the file is refused — never read as something else. A segment is sealed a frame for each batch of writes the primary sends, so a crash cuts off at most the frame being written, as it cut a record short before.

An archive is sealed or it is not; a file of the other kind in it is refused. FENEC_KEY_FILE names the key file too. Sealing and opening run at about 400 MB/s on one core, where writing the archive waits on the network and the disk. The database file itself stays plain: it is mapped and read where it lies, which encryption would take away — encrypt the disk it is on (LUKS, FileVault, an encrypted cloud volume).

Endpoints

Three endpoints — a --dir node's tenants each have them under /t/<tenant>/, and the node one more, the stream of every tenant — all on the HTTP listener and all wanting the replication token, not the --http-token.

GET  /_replication?since=<change>&history=<id>   the stream a replica is fed
GET  /_replication/status                          role, position, replicas, lag
POST /_replication/promote                         a replica's promotion
POST /_replication/tenants                         on a --dir node: every tenant's writes
                                                   over one stream (fenec archive --tenants)
Which token the endpoints want, and the status

All of them want Authorization: Bearer <replication token>, not the --http-token: the token is for replicas and for whoever promotes them. The status of a replica names its primary and says whether it is connected, how many writes it is behind the primary's disk, and how long ago it last heard from it; a primary's lists the replicas it is feeding and how far each has been sent.

Flags

--replication-token <value>turns replication on; needs --file, and --http to feed replicas. Also read from FENEC_REPLICATION_TOKEN
--replica-of http://host:portfollows that primary into --file; needs the token
--promoteopens a replica's file to take writes, forking its history
--replication-buffer <MiB>writes kept for replicas that fall behind, and for /_changes; default 64
--cdckeeps the writes for GET /_changes with no replica fed; on already with --replication-token

What it does not do

  • No synchronous replication. A write is answered when it is on the primary's disk, never when a replica has it.
  • No automatic failover of a single file. Promotion is by hand, and so is keeping the old primary from taking writes. A router's tenants can have one: nodes that take its lease stop writing when they lose it, and are failed over after (Automatic failover).
  • No TLS. The stream is plain HTTP, like every other listener here: across an open network, a tunnel carries it.
  • No object-storage client. An archive is a directory; a sync tool carries it to S3, R2 or GCS (In object storage).