Take it, prove it, restore it.
A backup is only worth the restore it produces. TillArk backs up through proven engines, then continuously proves the restore works — the wedge that turns “we take backups” into “our last N restores all passed.”
How a backup run works
A policy’s schedule queues a run, or you do (ark backups run). Each run writes one backup per target the policy places it on. An agent that has the source type’s programs claims each one, encrypts before upload and reports a signed manifest. TillArk records the manifest, never the data.
run ─▶ an agent that can run it claims the job
│
├─ opens the job's sealed credentials (in memory only)
├─ confirms the target's object lock, when the target requires one
├─ dumps or streams the source restic, or WAL-G or pgBackRest for point in time,
│ or clickhouse-backup for ClickHouse table parts
├─ encrypts in the agent, uploads the target only ever holds ciphertext
└─ signs the manifest Ed25519 + ML-DSA-65
│
▼
TillArk verifies both signatures, records the manifest (never the bytes)
status: succeeded · size · dedup · run epochDeduplication and compression come from restic, so a nightly dump stores only what changed. TillArk adds scheduling, retention, the manifest, and a prune that never races a running backup: see Schedules and upkeep. Backups of several sources queued together share a run epoch, so ark backups ls --epoch <n> shows the set taken together. A backup carries a status through pending → running → succeeded, then verifying → verified once a verification passes. A failed backup says why on the Backups page, under the table of ark backups ls, and as error in the API. Each backup also records when it read its data, shown on its row and returned as capture: see One moment.
Schedules and upkeep
A policy’s backup and verification schedules are five-field cron expressions, evaluated in UTC. A schedule that can never fire, such as 0 0 30 2 *, is refused when you save it. While an agent is connected, a slot is queued within about a minute of its time.
| When | What happens |
|---|---|
| Several slots were missed (no agent connected, an outage) | They run once, as a single catch-up backup, not one per missed slot. |
| A slot falls while a backup of the source to that target is still running | The slot waits and runs when that backup finishes. A source runs one backup per target at a time. |
| The source is paused or the policy is disabled | Nothing is scheduled, and the policy shows no next run. |
| A scheduled backup can’t be queued | The slot is skipped, the policy shows the reason, and the audit log records schedule skipped. The next backup that queues clears it, and so does changing the schedule. |
tilldev ark policies ls # CRON (UTC), NEXT, and any schedule error
tilldev ark policies get <policy-id>
tilldev ark policies update <policy-id> --cron "15 2 * * *" # 02:15 UTC daily
tilldev ark policies update <policy-id> --disableThe Policies page shows the same next run, last scheduled run and schedule error. In the API, they are next_run_at, last_scheduled_for and schedule_error on each policy.
To manage schedules, retention and the rest for every source from one reviewed file, see Policy files.
Backups that retention no longer keeps are deleted from their target by an agent. That delete never runs while a backup or restore of the same source is using the same target, and those wait while a delete is under way. If the agent doing it goes away, another agent takes the delete over after 30 minutes.
Restore-verification
On your policy’s verification schedule, an agent restores a real backup into a throwaway server of its own, checks what came back against the signed manifest, records how long it took, and removes the server. The server listens only on a private socket (Meilisearch on loopback with a random key), so nothing in your network can reach it, and your systems are never touched.
verify (on the policy's schedule, or on demand)
│
▼
a restorable backup the newest, or in turn an older one
│ never one still being written
▼
check the manifest signatures ──✗──▶ failed · nothing is restored
│ ✓
▼
restore into a throwaway server on the agent
│
▼
run the checks for its source type (below)
│
├─ any check fails, or none ran ──▶ failed
│
▼
read one part of the repository scheduled, restic: every blob against its hash
│
├─ damage found ──▶ failed · the damaged pack files are named
│
▼
passed · the backup becomes "verified" · the restore time is recorded
│
▼
the throwaway server and its files are removedEvery verification checks the manifest’s signatures and that the stored bytes match its signed hash and size. Then, per source type:
| Source | Restores into | Checks |
|---|---|---|
| PostgreSQL | A throwaway PostgreSQL, as a non-superuser. Foreign-data wrappers, servers and foreign tables are left out and named: their data lives outside the backup | Every table’s row count; tables, columns, constraints and indexes against the signed schema hash (same major version) |
| PostgreSQL, WAL-G | A throwaway PostgreSQL, replaying WAL to the backup’s end | WAL replays cleanly; the schema hash; every table read back in full; every data page’s checksum, when the cluster has checksums on |
| PostgreSQL, pgBackRest | A throwaway PostgreSQL, replaying WAL to the backup’s end | The stored backup against the signed manifest; every file and WAL segment against its checksum; then the same checks as WAL-G |
| MySQL / MariaDB | A throwaway mysqld or mariadbd | The dump loads without an error; every table’s row count |
| MongoDB | A throwaway mongod | Every collection and oplog entry against its CRC-64 checksum; every collection’s document count |
| Redis / Valkey | A throwaway redis-server or valkey-server | Every key restores; key counts per database (keys that expired since the backup are reported) |
| Meilisearch | A throwaway meilisearch | Every index loads; every index’s document count |
| ClickHouse | clickhouse-local, no server | Every table’s rows decode; every definition parses; every table’s row count |
| ClickHouse, clickhouse-backup | A throwaway ClickHouse server on the agent, reachable only from it | The stored backup against the signed manifest; every copied part restores and every table passes CHECK TABLE; the other tables’ rows decode; every definition parses; every table’s row count |
| TillForge git node | Scratch space on the git node, inside each organization’s encrypted directory | The stored backup against the signed manifest; every repository rebuilt and passes git fsck; every LFS object against its hash; every repository’s refs and LFS objects against the signed counts |
| Files | A temporary directory | Every file restores and re-reads against its hash; restored bytes match |
| Bucket | Nothing: objects are read back from the backup | Every object reads back; each matches the MD5 its bucket reported, where there is one; the object count against the signed manifest |
verified; a failed verification lands in your notifications. Because a real restore runs on a schedule, an expired credential or a changed bucket policy shows up before you need the backup.Run one by hand with tilldev ark verify <backup-id> and read the outcome with tilldev ark restores.
Verification at scale
Restoring the newest backup proves the newest backup. Three policy settings make scheduled verification cover everything you keep, without every source restoring at once:
| Setting | What it does |
|---|---|
Start within--verify-spread · default 60 min | Each policy starts at its own place within this many minutes after its slot, so policies on the same schedule don’t all restore at once. A policy keeps its place from slot to slot, and always starts before its next slot. 0 starts on the slot. |
Older backup--verify-older-every · default 4 | Every nth scheduled verification proves the older retained backup proven longest ago, never-proven ones first, instead of the newest. The others prove the newest until it passes. 0 proves the newest only. |
Data read in--verify-read-parts · default 30 | On restic repositories, each scheduled verification also reads one part of the stored data in full and checks every blob against its hash, the part read longest ago first. Over that many verifications every stored byte is read, including data only older backups use. 0 reads only what restores touch. |
A read locks its repository, so it waits while a backup, a prune or another read is using it, and new backups and restores of that repository wait while it reads. A read that can’t start within 20 minutes is left to a later verification, which the result names; the verification still passes on its restore. Damage a read finds fails the verification, with the damaged pack files named in the repository-read check. Proving an older backup doesn’t move the recovery point, which the newest backup sets.
tilldev ark policies update <policy-id> --verify-spread 120 --verify-older-every 7 --verify-read-parts 60
tilldev ark policies get <policy-id> # next verify, and what each verification covers
tilldev ark restores # PROVED: newest or older, and the part read
tilldev ark status # COVERED: kept backups proven, and data readThe Policies page shows each policy’s next verification and its settings, and the Restores page shows which backup each verification proved and the part it read. In the API they are verify_spread_min, verify_older_every, verify_read_parts and next_verify_at on each policy, and sampling on each restore. How far it has got is on the Recovery panel of the TillPulse overview and in tilldev ark status: how many kept backups a verification has proven, when the oldest unproven one was taken, and for each restic repository how many parts are read, any whose read failed, and, once all are read, since when every stored byte has been; coverage in GET /api/ark/status. Reading a part needs an ark-agent from this release or later; an older one restores the backup and reads no part, and a later verification by a current agent reads it.
Real restores are dangerous — and gated
A real restore overwrites a destination and, done carelessly, is a data-exfil vector (restore-to-arbitrary-target). So it carries the strongest gates in the product:
- Four-eyes approval. The requester must name a second, different owner/admin of the same org as approver, plus a non-empty
approval_ref(a change ticket / incident id). The server re-verifies the approver against the org’s members — the client’s claim is never trusted, and a token cannot approve its own request. - Allowlisted destination. You can only restore into a registered source in your own org — not an arbitrary URL.
- Verify before trust. The agent checks the manifest’s signatures, the snapshot it is about to read against the signed one, and the stored bytes against their signed hash before it touches the destination.
# A REAL restore overwrites a destination, so it is four-eyes gated:
# a DIFFERENT owner/admin's user id + an approval reference, and the
# destination must be an ALLOWLISTED registered source (exfil guard).
tilldev ark restore <backup-id> \
--into <destination-source-id> \
--approved-by <second-approver-uuid> \
--approval-ref INC-4821
# → authorized real restore <uuid> → source <dest> (pending)
# the agent verifies the manifest signature before trusting a byteHow a restore lands depends on the source type; in every case, what the backup doesn’t hold is left alone.
- PostgreSQL: one transaction, so it lands whole or not at all.
- Meilisearch: each index loads beside its live copy and all of them swap in at once.
- ClickHouse: tables load into a private staging database, are checked, then swap in one by one. With clickhouse-backup, the copied parts are first restored and checked in a throwaway server on the agent, and the rows it reads load the same way.
- MongoDB: collections are staged, then swapped in one at a time.
- MySQL and Redis: tables or keys are replaced one at a time.
- Files: restored in place under the agent’s
TILLARK_FILE_ROOTS. - Bucket: every object is written under the destination’s prefix with its content type and metadata, each write checked against the bucket’s MD5. The destination can also be a directory under
TILLARK_FILE_ROOTS.
Recover PostgreSQL to any moment
With the WAL-G or pgBackRest engine, the agent streams your database’s write-ahead log to every target that holds its backups, all the time. A restore can then land on any moment between your oldest base backup and a few minutes ago, not only on the moment a backup was taken. Each WAL stream reports how far it reaches: every commit before its recoverable-to time, on the database’s own clock, is archived.
| You need | Why |
|---|---|
| A self-managed PostgreSQL source with engine walg or pgbackrest | Both copy the data directory, which managed services don’t expose. |
The agent on the database host, with the source in TILLARK_LOCAL_SOURCES and the engine in TILLARK_ENGINES | Base backups and the WAL stream run only on the host that lists the source, and only with an engine it lists. List the source on one host: a second one is told which agent holds it. pgBackRest must be 2.51 or newer. |
| The REPLICATION attribute and a replication line in pg_hba.conf | The stream is a replication connection with its own slot, so Postgres keeps WAL until it is archived. |
EXECUTE on pg_backup_start and pg_backup_stop | The role starts and stops each base backup. The agent checks before every backup and, if it can’t, fails it with the GRANT to run. |
EXECUTE on pg_switch_wal() | A quiet database fills segments slowly. The agent closes one at least every half of the policy’s RPO (30 s to 15 min; 150 s without an RPO), so the archive keeps up. |
max_slot_wal_keep_size | If a stream stalls, the slot holds WAL back. The cap protects the disk; if Postgres drops the slot, TillArk records a gap. |
| An object-store target, or an SFTP target with pgBackRest | WAL-G can’t verify an SFTP server’s host key, so it writes only to object storage. pgBackRest checks the host key registered with the target, and needs its path given from the root. |
# On the database host, as the postgres OS user (the engine reads the data directory):
TILLARK_ENGINES=restic,walg # or restic,pgbackrest (pgBackRest 2.51 or newer)
TILLARK_LOCAL_SOURCES=<source-id> # the sources whose WAL this host archives
-- The role in the source's connection string:
ALTER ROLE ark REPLICATION;
GRANT pg_read_all_settings TO ark;
GRANT EXECUTE ON FUNCTION pg_backup_start(text, boolean), pg_backup_stop(boolean) TO ark;
-- before PostgreSQL 15: pg_start_backup(text, boolean, boolean), pg_stop_backup(boolean, boolean)
GRANT EXECUTE ON FUNCTION pg_switch_wal() TO ark;
-- pg_hba.conf: allow it a replication connection, e.g.
-- host replication ark 127.0.0.1/32 scram-sha-256
-- postgresql.conf: cap what a stalled stream may hold back
-- max_slot_wal_keep_size = 20GBThe stream to a target starts with its first base backup, which waits for the stream before it begins so its own WAL is kept. Pausing a source stops new base backups but keeps WAL streaming, so point-in-time recovery stays current and the slot never grows unbounded. Pausing a target stops its stream.
Restore to a moment from the Restores page (Real restore → A point in time) or the CLI. TillArk refuses, with the reason, a moment in the future, before the chosen backup finished, past what is archived, or across a gap. The agent recovers in a private sandbox and copies the database into the destination in one transaction only when recovery succeeds, then reports the last commit it replayed. A moment with no later commit, such as a quiet night, restores too: once the archive provably holds every commit up to it, recovery finishes at the end of the archive. Verifications always recover to the end of their backup.
# Recover to a moment. TillArk picks the newest base backup that finished
# before it and whose engine's WAL archive reaches it, and replays WAL up to it. Four-eyes gated, as above.
tilldev ark restore --source <source-id> --at 2026-09-28T14:05:00Z \
--into <destination-source-id> \
--approved-by <second-approver-uuid> \
--approval-ref INC-4822
tilldev ark wal # per target and engine: state, recoverable-to time, lag, gaps
tilldev ark restores # TO: the moment asked for, or the last commit replayedIf you stop archiving a source on a host, remove it from TILLARK_LOCAL_SOURCES and drop its inactive tillark_ replication slots there, or Postgres keeps their WAL.
Incremental and differential base backups
A large database need not be copied whole for every base backup. On WAL-G and pgBackRest sources, a policy can have each base backup store only what changed:
| Policy setting | Each base backup stores | A restore reads |
|---|---|---|
off | The whole database (the default). | That backup. |
differential | What changed since the last full backup. | The full backup and that one. |
incremental | What changed since the backup before it. | The full backup and every one after it, up to that one. |
# Each base backup stores what changed since the one before it; a full one at least weekly.
tilldev ark policies update <policy-id> --incremental incremental --full-every 7d
# Or only what changed since the last full backup: a restore reads two backups at most.
tilldev ark policies update <policy-id> --incremental differential
tilldev ark backups ls --source app-db # KIND: full, incremental on 3f2a91c0, …A full backup is taken at least every full_every days (default 7), and once a chain holds 64 backups on WAL-G or 1000 on pgBackRest. TillArk picks what each backup builds on from the signed, unquarantined backups of the same source, target, engine and key: the newest full one for a differential backup, the newest in that full one’s chain for an incremental one. Before building on it, the agent checks the stored backup (with WAL-G, every backup its chain holds) against its signed manifest. If it has changed, or is gone, the agent takes a full backup instead, and the catalog says why. The signed manifest records what each backup builds on, and TillArk accepts it only when it is the backup it handed out.
A restore or verification reads every backup its backup builds on and checks each file it reads against the signed manifest, so a file changed anywhere in the chain fails it. A backup stays while anything kept builds on it, whatever its retention says. A backup that builds on a quarantined one is quarantined with it, and dismissing the alarm releases them together. A backup started by hand follows the source’s policies when they agree, and is full when they don’t. Chains need an ark-agent from this release; an older agent takes full backups.
Copy ClickHouse table parts
On a self-managed ClickHouse server, the clickhouse-backup engine (chbackup) copies each MergeTree table’s data parts instead of reading every row, which takes far less of the server on large tables. It freezes the tables, a snapshot made of hard links that copies nothing on the server, then packs the parts, the other tables’ rows and every definition into one backup, encrypted in the agent like any other.
| Table | Backed up as |
|---|---|
| MergeTree tables on local disks, and materialized views’ own tables | Their data parts |
Tables set up for lightweight UPDATE (enable_block_number_column = 1) | Rows. Freezing leaves out the patch parts those updates write |
| Tables on object-storage or encrypted disks | Rows |
| Tables whose database or table name needs quoting | Rows |
| Every other engine: Log, Memory, Set, Join and the like | Rows, as on the default engine |
| You need | Why |
|---|---|
| A self-managed ClickHouse source with engine chbackup | The parts are read from the server’s disks, which managed services don’t expose. |
The agent on the database host, with the source in TILLARK_LOCAL_SOURCES and chbackup in TILLARK_ENGINES, running as a user that can read and write ClickHouse’s data directory | Backups run only on the host that lists the source. The snapshot is removed from the server when the backup ends. |
clickhouse-backup 2.5 or newer, and the clickhouse program | Verifications and restores replay the parts in a throwaway ClickHouse server on the agent. |
A ClickHouse user that is not readonly, with SELECT, SHOW and ALTER FREEZE PARTITION | Freezing a table is an ALTER, which a readonly user can’t run. The agent checks first and names the fix. |
| On a restore destination, every storage policy the tables use | Tables are created with their policies. TillArk checks before it changes anything. |
# On the database host, as a user that can read and write ClickHouse's data directory:
TILLARK_ENGINES=restic,chbackup
TILLARK_LOCAL_SOURCES=<source-id> # the ClickHouse sources this host backs up
TILLARK_CHBACKUP_BIN_DIR=/usr/local/bin # where clickhouse-backup is, if not on PATH
-- The user in the source's connection string (not readonly):
GRANT SELECT, SHOW, ALTER FREEZE PARTITION ON *.* TO ark;
tilldev ark sources update events --engine chbackupALTER TABLE … UPDATE or DELETE) still running then is not run again. The backup’s row count also includes rows a lightweight DELETE hid that merges hadn’t removed yet, so it can be higher than a restore’s. A table that took lightweight updates and then had enable_block_number_column turned off is refused until ALTER TABLE … APPLY PATCHES folds them in.Back up a TillForge git node
A TillForge git node source backs up a git server’s repositories with its own tillforge backup: every repository’s history, its Git LFS objects and deleted repositories awaiting purge, each sealed under its organization’s key before the agent reads it, then encrypted in the agent like any other backup. The manifest signs how many refs and LFS objects each repository holds.
| You need | Why |
|---|---|
The agent on the git node, running as the node’s user, with the source in TILLARK_LOCAL_SOURCES | The node reads its repositories and fetches its organizations’ keys itself. Backups and verifications run only on the host that lists the source; a restore runs only on the host that lists its destination. |
The locator tillforge:///path/to/tillforge.env, optionally with ?org=<id> or ?org=<id>&repo=<id> | The node’s environment file, which must belong to root or the node’s user and be writable by no one else. The query narrows a source, or a restore destination, to one organization or repository. |
A target in the node’s region, and a policy with --residency and --allow-same-region | Copies go to another region by default. Pinning them keeps a node’s backups where its repositories are stored. |
# On the git node, as the node's user:
TILLARK_LOCAL_SOURCES=<source-id> # the git nodes this host backs up
TILLARK_FORGE_BIN_DIR=/usr/local/bin # where tillforge is, if not on PATH
tilldev ark sources create --name git-eu-1 --type forge --provider self --unmanaged --with-dsn
# tillforge:///etc/tillforge/tillforge.env
tilldev ark policies create --source <source-id> --cron '0 1 * * *' --verify-cron '0 4 * * *' \
--residency eu --allow-same-regiongit fsck on it. Each LFS object is checked against its hash, and each repository’s refs and LFS objects against the signed counts. TillForge shows the newest verified backup on its Encryption page. A restore first proves the stored backup and its counts, so a damaged one never touches the node; then it puts missing repositories in place and replaces one that differs, keeping the previous copy in the node’s trash. Repositories whose organization’s key was erased are reported as unreadable and not restored.Restore a run’s stores together
Backups queued for the same scheduled slot form a run. A run’s page (open it from the Backups page), tilldev ark runs or /api/ark/runs/{epoch} verifies or restores its stores as one set, says whether they come back as of one moment, and checks the references you declare between them.
| What comes back | Who | |
|---|---|---|
| Verify together | Every store in the run, each in its own sandbox, as of the run’s point where its backup allows it. Nothing you run is touched. | An owner or admin |
| Restore together | The stores you pick, each into a registered source of the same type, as of the run’s point where its backup allows it. | An owner or admin, with a second approver as for any real restore |
One moment
A run is consistent when every store comes back as of the same moment, the run’s point. How each store was read decides whether it can:
| Store | Read | Comes back |
|---|---|---|
| PostgreSQL on WAL-G or pgBackRest | A base backup, then archived WAL | As of any moment its WAL reaches |
| PostgreSQL | One snapshot | As of that moment |
| MySQL / MariaDB | One transaction | As of that moment when every table is InnoDB or MyRocks; otherwise over time |
| MongoDB | The whole replica set, with its oplog | As of that moment; one database, or a server that isn’t a replica set, over time |
| Versioned bucket | Every version current from five minutes before the run until the backup began | As of any moment in that window |
| Unversioned bucket, directories | Object by object | Over time |
| Redis / Valkey, ClickHouse, Meilisearch, git node | Key by key, table by table, index by index, repository by repository | Over time |
The point is the moment of the one store read as a snapshot, when there is one; otherwise the latest moment every versioned bucket covers, or the latest base backup’s end. A run holding a store read over time, or two stores each read as its own snapshot, isn’t consistent: the run’s page, tilldev ark runs show and the API name each such store and say why, and the others still come back at the point.
The agent times every backup on TillArk’s clock, measured on each request it makes, and reads each database’s and bucket’s own clock, so a point-in-time database and a versioned bucket recover to the point on their own clocks. The plan shows how far apart any two stores’ data may be from clock uncertainty: tens of milliseconds between databases, and about a second with a bucket, whose clock is read to the second. A versioned bucket restores each object’s version current at the point; an object gone by then comes back from its nearest version within that uncertainty. So when your application stores an object before the row that names it, and removes the row before the object, no restored row names a missing object, and the references you declare check exactly that.
The plan shows each store’s moment before anything starts. A store whose backup can’t be restored is left out, with the reason. When a base backup finished after the point, an older one that reaches it is used; when none does, that database comes back to its own end, and the plan says so. A run passes when every store passed and every reference was checked and found whole.
References
A reference declares that a column of a PostgreSQL source names objects in an object source (a bucket or a directory). Declare it on the database’s source page or with tilldev ark sources refs.
| Field | Meaning |
|---|---|
table | schema.table. A bare name is the table in public, else in the one schema that has it. Its partitions and inheriting tables are read too, each row once; foreign ones are not, and the result names them. Views are not read. |
column | Any scalar column, read as text. In an array each element is a value; JSON gives its string, or its array of strings. |
to | The object source the values name. |
strip | Optional. Only values starting with it name an object, less the prefix, such as a CDN address. |
key | How a value becomes an object key, with {value} once: avatars/{value}. Defaults to {value}. |
A verification checks a reference in the database’s sandbox against the object backup, after checking the object backup against its signed hash. A restore checks it once both stores are restored, on an agent that reaches both destinations. The agent reads the column from one read-only snapshot, as text, never through a cast the database defines. Neither side has to fit in memory.
A restore’s check reads as the role its database’s source connects with. When row-level security would hide rows from that role, the check fails and says so rather than checking only the rows it could see: connect as the table’s owner or a role that bypasses row-level security.
Each check reports the rows and values it read, values that name no object (empty, or without the strip prefix), the objects listed, the missing keys and the rows naming them, with up to five examples. Any missing key fails the run and raises a run.dangling signal; every finished run raises run.passed or run.failed.
# A PostgreSQL column that names objects in an object source:
tilldev ark sources refs app-db add --name avatars --column users.avatar_url \
--to uploads --strip https://cdn.example.com/ --key 'avatars/{value}'
tilldev ark runs # each run, and when its stores last came back together
tilldev ark runs verify 1790208000 # every store in a sandbox, references checked
tilldev ark runs show 1790208000 # the plan, each store's outcome, missing objects
tilldev ark runs restore 1790208000 \
--into app-db=app-db-restore,uploads=uploads-restore \
--approved-by <second-approver-uuid> --approval-ref INC-4823Recovery point, measured
Each source shows its recovery point: the newest moment a restore could bring back right now. It comes from the newest backup that is signed, not quarantined and restorable, or, for a source on WAL-G or pgBackRest, from how far its WAL archive reaches past a base backup. A backup that failed, or one you can’t restore, never counts.
| How it works | |
|---|---|
| Objective | A policy’s RPO objective: how much recent data a restore may lose. With several policies on a source, the tightest one applies. |
| Default | The schedule’s longest gap plus time for the backup to run (a quarter of the gap, at least 15 minutes). An hourly schedule gets 1 h 15 min, a nightly one 1 day 6 h. A source that streams WAL gets 5 minutes. |
| Feasibility | An objective shorter than the schedule’s longest gap is refused, since no backup could meet it. With WAL streaming the floor is 1 minute. 0 sets no objective. |
| Breach | When the recovery point grows older than the objective, Sources and the overview turn red and your notifications say which source and since when. A source that never backed up is judged from when its policy began. A paused source is never in breach. |
| RTO | A policy’s restore time objective, judged against how long the latest restore took, verifications included. |
tilldev ark sources ls # RECOVERY POINT: age / objective
tilldev ark sources get <source-id> # recovery point, objective, breached since
tilldev ark policies update <policy-id> --rpo 7200GFS retention
Retention is grandfather-father-son: keep the last N, plus a thinning tail of hourly / daily / weekly / monthly / yearly snapshots. Retention depth is a correctness property — it’s what lets you roll back past a logical corruption (a “time-bomb” a fresh backup would faithfully preserve).
Retention applies per target: each target keeps its own full set, so a second target adds depth rather than splitting it. With several policies on one source, a backup is deleted only when every policy would let it go. A backup that another kept backup builds on stays until nothing kept builds on it. Retention is applied about every 10 minutes.
# GFS defaults — override any bucket with a --keep-* flag on the policy.
tilldev ark policies create --source <id> \
--keep-last 7 --keep-hourly 24 --keep-daily 7 \
--keep-weekly 4 --keep-monthly 12 --keep-yearly 3
# Change one bucket later; the others stay as they are.
tilldev ark policies update <policy-id> --keep-daily 14Limits by source type
| Source | Know before you rely on it |
|---|---|
| PostgreSQL | Point-in-time recovery needs a self-managed server and the WAL-G or pgBackRest engine. pg_dump must be at least as new as the server. Changing a source’s engine or key starts a new WAL archive: moments before the change are recovered from the old one. |
| MySQL / MariaDB | A real restore is not one transaction: tables are replaced one at a time. |
| MongoDB | A backup reaches one point in time only on a replica set, where it includes the oplog. Users and roles are not restored. Time-series collections restore onto 7.0.3 or later. Stored JavaScript and sharded collections are refilled in place rather than swapped. |
| ClickHouse | A restore brings back the rows queries read at backup time: a mutation still running then is not run again. With clickhouse-backup, tables on object-storage or encrypted disks, tables set up for lightweight UPDATE and tables whose names need quoting are backed up as rows, and a restore destination needs every storage policy the tables use. |
| Redis / Valkey | Redis Cluster is not supported; add each shard’s primary as its own source. Keys that expire between backup and restore are skipped. |
| Meilisearch | API keys, task history and embedder API keys are not backed up. |
| TillForge git node | Only the node’s repositories, LFS objects and deleted repositories awaiting purge are backed up; accounts, settings and pull requests live in TillForge’s database, a PostgreSQL source of its own. A repository whose organization’s key was erased can’t be read or restored. |
| Bucket | From an unversioned bucket only current objects are backed up; from a versioned one, the versions current from five minutes before the run until the backup began, and a restore writes one version of each object. Content headers and user metadata come along; tags, ACLs and per-object lock settings don’t. A bucket holding objects encrypted with customer-provided keys (SSE-C) can’t be backed up. Objects uploaded in parts, or encrypted with KMS, carry no MD5 to compare, so they are checked against the backup’s own hashes only. Objects deleted while an unversioned bucket is backed up are skipped. A files backup restores only into a directory. |
| Object lock | Available on S3, B2, Wasabi and MinIO targets. In GOVERNANCE mode a key with bypass rights can still delete early; see Security. |
Next: Failover for promoting a replica safely, or Security for signed manifests and object lock. Back to the TillArk overview.