---
title: "Retention"
description: "One rule with a floor under it, fixed when the backup is created."
url: "https://saved.sh/docs/backups/retention"
---

Retention 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 [#the-rule]

An artifact is expired when it is **both**:

1. older than `expire_after`, **and**
2. outside the newest `keep_last`.

```yaml
retention:
  expire_after: 90d    # nothing older than 90 days
  keep_last: 10        # but always keep the 10 most recent, however old
```

The 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 [#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 [#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 [#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 [#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 [#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.

<Callout type="warn">
  Decide retention before the first run. It is one of only two fields you cannot change later,
  the other being `kind`.
</Callout>

## Protection: `lock_for` [#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.

```yaml
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.

<Callout type="error">
  **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.
</Callout>

If you need storage-layer immutability, the honest answer today is to
[deliver to your own bucket](/docs/backups/delivery) and configure object lock on it
yourself, where the enforcement is under your control and not ours.

## The sweep [#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_after` is skipped entirely.** Its artifacts are never even
  examined.

### Retention in your own bucket [#retention-in-your-own-bucket]

The sweep deletes from a destination &#x2A;*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: &#x2A;*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 [#deleting-an-artifact-yourself]

```bash
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.

## Next [#next]

<Cards>
  <Card href="/docs/backups/delivery" title="Delivery" description="Where the copies the sweep deletes actually live." />

  <Card href="/docs/billing/usage" title="Usage" description="What holding an artifact costs." />

  <Card href="/docs/recover/rehearsal" title="Rehearsal" description="Checking the artifact you kept still restores." />
</Cards>
