Retention
The one setting that bounds what you store, fixed when the backup is created.
View as MarkdownRetention decides how long artifacts live. It is the only setting that bounds what you store, and therefore the only one that bounds what you pay to hold.
A backup with no retention keeps everything forever. That is the default, and it is the right default, but it is not free.
The rule
An artifact is expired when it is both:
- older than
expire_after, and - outside the newest
keep_last.
retention:
expire_after: 90d # nothing older than 90 days
keep_last: 10 # but always keep the 10 most recent, however oldThe two combine with and, never with or.
| Field | On its own, deletes | Is |
|---|---|---|
expire_after | Everything older, subject to the floor | The age limit |
keep_last | Nothing | A floor, exempting the newest N |
lock_for | Nothing. It only forbids | A protection window |
keep_last is a floor, not a cap
This is the most misread thing in the product.
keep_last: 5 on its own keeps every artifact forever, exactly as if retention were off.
It does not mean "keep only five".
The reason is a failure we would rather not ship. Read as a cap, keep_last: 5 deletes your
sixth-newest artifact the moment a seventh appears. A source that suddenly runs more often
than expected, or a burst of manual triggers during an incident, would quietly destroy every
copy older than this afternoon. A floor cannot do that.
There is no hard cap on count, deliberately. Bound by age, and use keep_last to stop the age
bound biting during a quiet period.
Two things are never deleted
Whatever the policy says:
- The newest remaining artifact. A backup that has run at least once always has something to restore from.
- Any artifact still inside its lock window.
Retention is write-once
Fixed when you define the backup. Not editable afterwards, not loosened, not tightened, not turned off. A different policy means a new backup.
Decide retention before the first run. It is one of only two fields you cannot change later, the other being kind.
That constraint is the point rather than a limitation. The retention shown on a backup is the promise made to artifacts already archived under it. A number you could quietly raise next quarter, after an auditor asked, would not be a promise. Making it immutable is what makes it worth reading.
Trial workspaces cap retention at 7 days. Because retention is write-once, a backup created on trial keeps that policy even after you upgrade. If you intend a long retention, set it on a paid plan.
Protection: lock_for
lock_for marks new artifacts as protected for a window, during which our delete path and our
retention sweep both refuse to remove them.
| Property | Behaviour |
|---|---|
| Applied | Per artifact, at archive time |
| Affects | Future artifacts only. Never retroactive |
| Expiry | Lapses on its own |
| Constraint | May never exceed expire_after |
A policy that forbids a deletion it also requires is a contradiction, and is refused at definition.
What a lock actually stops. It is an application-level hold: it stops deletion through our API, our dashboard, our support tooling and our retention sweep. It is not object-lock enforcement at the storage layer, so it is not a defence against somebody holding the archive credentials directly. Treat it as protection against mistake and misuse, not as a compliance-grade WORM guarantee.
If you need storage-layer immutability, deliver to your own bucket and configure object lock there, where enforcement is under your control.
The sweep
Expiry is done by a daily sweep, not at run time. Nothing in the backup path deletes anything.
- Expiry is not instant. "90 days" is 90 days plus up to one day.
- A backup with no
expire_afteris skipped entirely. Its artifacts are never examined. - In your own buckets, the sweep deletes only where you granted
allowDelete, which is off by default. With it off, that bucket grows forever and clearing it is your job.
Worked examples
A daily backup, one year in, on day 366.
| Policy | Kept |
|---|---|
| none | All 365 |
keep_last: 10 | All 365. Nothing expires without expire_after |
expire_after: 90d | The newest 90 |
expire_after: 90d, keep_last: 10 | The newest 90. The floor is already satisfied |
expire_after: 7d, keep_last: 30 | The newest 30. The floor binds |
The last row is where keep_last earns its place: when the age limit is tighter than the
number of copies you want on hand.