Retention
One rule with a floor under it, 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 are billed for holding it.
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, it deletes | What it means |
|---|---|---|
expire_after | Everything older, subject to the floor | The age limit |
keep_last | Nothing | A floor: the newest N are exempt from the age limit |
lock_for | Nothing, it only forbids | A protection window, see below |
keep_last is a floor, not a cap
This is the single most misread thing on the page, so it is worth stating twice.
keep_last: 5 on its own keeps every artifact forever, exactly as if retention were off.
It does not mean "keep only five". Without expire_after, nothing expires at all.
The reason is a failure mode 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 you expected, or a burst of manual triggers during an incident, would quietly
destroy every copy older than this afternoon. A floor cannot do that.
If you want a hard cap on count, there is not one, and that is deliberate. Bound it by age
instead, 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, even when every artifact is past
expire_after. - Any artifact still inside its lock window.
Worked examples
Assume a daily backup that has been running for a year, and it is 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, because age alone would leave 7 |
expire_after: 1d | The newest 1. The age rule takes everything else, and the newest is never taken |
The fifth row is the useful one: keep_last earns its place when the age limit is tighter
than the number of copies you want on hand.
Durations
expire_after and lock_for take a duration with a unit suffix.
| Written | Means |
|---|---|
90d | 90 days |
720h | 30 days |
36h | 36 hours |
Days, hours, minutes and seconds are accepted. The value must be positive: 0d is refused
rather than treated as "off". To turn retention off, omit the field.
Retention is write-once
A backup's retention is fixed when you define it and cannot be edited afterwards, not
loosened, not tightened, not turned off. The API answers 409 retention_immutable to
anything that would change it, and the dashboard renders a set policy as text with no form.
Re-sending the policy already stored is accepted, so a client that echoes the whole definition back is not an error.
To back the same source up under a different policy, create a new backup.
That constraint is the point rather than a limitation. The retention shown on a backup is the promise made to the 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.
Decide retention before the first run. It is one of only two fields you cannot change later,
the other being kind.
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.
retention:
expire_after: 90d
lock_for: 30d| Property | Behaviour |
|---|---|
| Applied | Per artifact, stamped at archive time |
| Affects | Future artifacts only. Never retroactive |
| Expiry | The lock lapses on its own; the artifact becomes deletable again |
| Constraint | lock_for may never exceed expire_after |
The last row is refused at definition with lock_exceeds_expiry. A policy that forbids a
deletion it also requires is a contradiction, not a configuration.
A locked artifact also blocks deletion of its parent backup, which is what stops a delete of the definition being a way around the lock.
What lock_for protects against, precisely. 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 who holds 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, the honest answer today is to deliver to your own bucket and configure object lock on it yourself, where the enforcement is under your control and not ours.
The sweep
Expiry is done by a daily sweep, not at run time. Nothing in the backup path deletes anything.
| When | Once a day, at 03:00 UTC |
| Scope | Every backup with an expire_after set |
| Order | Expire the artifact record, then remove the copies |
Two consequences worth knowing:
- Expiry is not instant. An artifact past its window survives until the next sweep, so "90 days" is 90 days plus up to one day.
- A backup with no
expire_afteris skipped entirely. Its artifacts are never even examined.
Retention in your own bucket
The sweep deletes from a destination only where you granted allowDelete, and that grant
is read live at sweep time.
allowDelete | What the sweep does to that copy |
|---|---|
| On | Deletes it alongside ours |
| Off (default) | Leaves it, and counts it as skipped rather than failed |
Stated plainly: with allowDelete off, that bucket grows forever, and clearing it out is
your job. That is the default because destroying data in a bucket you own should take an
explicit grant, not an inherited policy.
Deleting an artifact yourself
sctl artifact delete <artifact-id>Refused while the artifact is locked. Otherwise it removes our copy; copies in your own buckets are left where they are, because we hold write credentials rather than a mandate to destroy your data.