# Lifecycle policies

Every STRG bucket is versioned, all the time, which makes a good safety net: overwriting an
object keeps the old version, and deleting an object writes a delete marker and keeps every
version behind it. All of those versions are billed as stored data until something
removes them.

Lifecycle rules are that something. A rule tells us how long to keep things under a key prefix,
and we tidy up on schedule, so old data leaves the bucket - and the bill - without anyone having
to remember it. Without rules, objects and their previous versions are kept until you delete
them.

Some situations where a rule earns its place:

- **A nightly backup** that overwrites the same keys every night: a rule keeps a month of previous
  versions to restore from, and lets older ones go.
- **Logs or scratch data** under `logs/` or `tmp/`, useful for a few days and then not at all.
- **Large uploads** from an app or an agent that sometimes stop halfway: a rule clears away the
  parts of uploads that were never completed, which are otherwise stored and billed.

## What a rule does

A rule applies to every object whose key starts with its **prefix**, and an empty prefix is the
whole bucket. Each rule carries up to three actions - at least one, since a rule without an
action would have nothing to do. Here is what each one does, and where to find it in the
dashboard and in S3's XML:

| Action | Dashboard field | S3 element | Effect |
| --- | --- | --- | --- |
| Expire the current version | *Expire the current version after* | `Expiration` / `Days` | N days after an object was uploaded, its current version becomes a previous version and the object stops being listed. The data is still stored and billed until a *delete previous versions* action removes it |
| Delete previous versions | *Permanently delete previous versions after* | `NoncurrentVersionExpiration` / `NoncurrentDays` | Deletes a previous version permanently, N days after it stopped being current. This is the action that frees storage |
| Abort incomplete multipart uploads | *Abort incomplete multipart uploads after* | `AbortIncompleteMultipartUpload` / `DaysAfterInitiation` | Aborts a multipart upload that was not completed within N days of starting, and discards its parts |

A few details about how the actions behave:

- Days are whole numbers, at least 1.
- Rules run once a day, so an action happens at the first daily run after its number of days has
  passed rather than at the exact hour.
- A rule also tidies up a delete marker once no versions remain behind it.
- Lifecycle rules cannot remove a locked version. A version under an
  [Object Lock](https://docs.strg.com/object-locks) retention period or legal hold is skipped, stays billed, and is
  removed once its lock has expired. See [Alongside Object Lock](https://docs.strg.com/lifecycle-policies#alongside-object-lock).

**Danger: Deleted versions are gone for good**

*Delete previous versions* deletes data permanently. Combined with *Expire the current version*
on the same prefix, it deletes every object under that prefix once both periods have passed -
and with an empty prefix, every object in the bucket. Deleted versions cannot be restored.

## Recommended rules

We suggest two rules on every bucket, because they pay for themselves quietly:

| Action | Suggested days | Why |
| --- | --- | --- |
| *Permanently delete previous versions after* | 14 | Versioning is always on, so every overwrite and every delete leaves a previous version behind, and that version is billed. This action is the only thing that frees it |
| *Abort incomplete multipart uploads after* | 7 | An upload that stopped halfway leaves parts that are stored and billed until something clears them away |

Those numbers are a starting point, and a different one fits when the use case is known:

- **Behind a backup tool that prunes its own data**, such as Kopia or restic: 1 day for previous
  versions. These tools delete long after they wrote the data, so by the time a version stops being
  current, the tool has already decided it is no longer needed.
- **On scratch prefixes** such as `tmp/`: 1 day for both, as in the [`tmp/` recipe](https://docs.strg.com/lifecycle-policies#expiring-a-tmp-prefix).
- **For files people edit by hand**, a longer window, such as the 30 days in the
  [first recipe](https://docs.strg.com/lifecycle-policies#keeping-30-days-of-old-versions), leaves more time to notice and recover a
  mistake.

The days for previous versions count from the day the version **stopped being current** - the day
it was overwritten or deleted - and not from the day it was uploaded. A file that sat untouched for
a year and is overwritten today still has its previous version for the full number of days.

### Alongside Object Lock

On a bucket with [Object Lock](https://docs.strg.com/object-locks), the two clocks are independent. A lock runs from
the upload, a rule's days run from the moment a version stopped being current, and a rule never
removes a version that is still locked, whatever its days are. That version is skipped and removed
once its lock expires. The number of days therefore does not need to match the retention period, and
setting them equal is not the aim: the lock protects the version, and the rule tidies up after it.

## Limits

STRG checks each rule as it is saved, so a mistake shows up straight away rather than as a
surprise weeks later. These are the bounds a bucket's rules stay within:

| Limit | Value |
| --- | --- |
| Rules per bucket | 100 |
| Rules per prefix | 1. All actions for one prefix go in one rule |
| Overlapping prefixes | Kept apart: no prefix may start with another (`logs/` and `logs/app/` overlap), and a rule for the whole bucket must be the only rule |
| Prefix length | 1,024 bytes of UTF-8. No control characters |
| Rule ID (S3 API) | 255 characters, unique within the bucket - including the `_marker` suffix described under [Rule IDs](https://docs.strg.com/lifecycle-policies#rule-ids). No control characters |

Amazon S3 allows overlapping prefixes. STRG keeps them apart, which makes rules easy to read:
every object falls under at most one rule, so it is always clear which days apply to it.

## In the dashboard

The dashboard is the quickest way to set up a rule, and it shows the bucket's rules in plain
words. Here is how it goes:

1. A bucket's settings live on its page in the dashboard at `https://app.strg.com`: on a
   narrower screen they are the **Settings** tab, and on a wide one they sit beside the object
   browser.
2. The **Lifecycle rules** card lists the bucket's rules, and its **Edit** button opens the
   editor.
3. **Add a rule** adds one. Its **Objects whose key starts with** field takes a prefix such as
   `logs/`; left empty, the rule covers the whole bucket.
4. Each action takes a number of days, and an empty field means *Never*.
5. **Save rules** saves them.

Anything that needs a second look - an overlap, a rule with no action - is shown right under the
rule, and **Save rules** waits until it is sorted out. Saving replaces every rule on the bucket,
so saving with no rules removes them all.

The [Ask our AI Assistant](https://docs.strg.com/assistant) can fill in this form for you from a description
such as "keep old versions for 30 days"; pressing **Save rules** stays with you.

The dashboard and the S3 API edit the same configuration, so rules set through one show up in
the other.

### A configuration the dashboard cannot show

If a bucket's configuration contains something the form has no field for, the card shows a
warning naming it and offers no **Edit** button, and that bucket's rules are managed through the
S3 API instead. This keeps your rules safe: showing only part of the configuration would mean a
save from the dashboard deletes the rules it did not show.

## Through the S3 API

Three operations read and write a bucket's rules, each with the access key permission it needs:

| Operation | Needs the permission |
| --- | --- |
| [`PutBucketLifecycleConfiguration`](https://docs.strg.com/s3-api/PutBucketLifecycleConfiguration) | *Manage lifecycle rules* (`bucket-lifecycle:write`) |
| [`GetBucketLifecycleConfiguration`](https://docs.strg.com/s3-api/GetBucketLifecycleConfiguration) | *Read lifecycle rules* (`bucket-lifecycle:read`) |
| [`DeleteBucketLifecycle`](https://docs.strg.com/s3-api/DeleteBucketLifecycle) | *Manage lifecycle rules* (`bucket-lifecycle:write`) |

Both permissions can be held by a key scoped to specific buckets;
[Access keys](https://docs.strg.com/access-keys) has the details. When an agent works through the
[MCP server](https://docs.strg.com/getting-started-mcp) with a key that does not hold *Act without confirmation*
(`mcp:skip-confirmation`), we ask you to confirm before it replaces a bucket's lifecycle rules,
since a rule can delete data permanently.

`PutBucketLifecycleConfiguration` replaces the bucket's whole configuration. This one expires
everything under `tmp/` after a day, and keeps previous versions under `backups/` for 30 days:

```xml title="lifecycle.xml"
<?xml version="1.0" encoding="UTF-8"?>
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Rule>
    <ID>backups</ID>
    <Filter><Prefix>backups/</Prefix></Filter>
    <Status>Enabled</Status>
    <NoncurrentVersionExpiration><NoncurrentDays>30</NoncurrentDays></NoncurrentVersionExpiration>
    <AbortIncompleteMultipartUpload><DaysAfterInitiation>7</DaysAfterInitiation></AbortIncompleteMultipartUpload>
  </Rule>
  <Rule>
    <ID>tmp</ID>
    <Filter><Prefix>tmp/</Prefix></Filter>
    <Status>Enabled</Status>
    <Expiration><Days>1</Days></Expiration>
    <NoncurrentVersionExpiration><NoncurrentDays>1</NoncurrentDays></NoncurrentVersionExpiration>
    <AbortIncompleteMultipartUpload><DaysAfterInitiation>1</DaysAfterInitiation></AbortIncompleteMultipartUpload>
  </Rule>
</LifecycleConfiguration>
```

STRG needs the request to carry `Content-MD5` or an `x-amz-checksum-*` header, and answers
`400 InvalidRequest` without one. The AWS CLI and the AWS SDKs add one automatically. Another HTTP
client sends `Content-MD5`, the base64 MD5 digest of the body
(`openssl dgst -md5 -binary lifecycle.xml | openssl base64`).

The same configuration from each tool:

**AWS CLI**

```json title="lifecycle.json"
{
  "Rules": [
    {
      "ID": "backups",
      "Filter": { "Prefix": "backups/" },
      "Status": "Enabled",
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
    },
    {
      "ID": "tmp",
      "Filter": { "Prefix": "tmp/" },
      "Status": "Enabled",
      "Expiration": { "Days": 1 },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 1 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
    }
  ]
}
```

```sh
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-bucket \
  --lifecycle-configuration file://lifecycle.json \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1

aws s3api get-bucket-lifecycle-configuration \
  --bucket my-bucket \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

s3.put_bucket_lifecycle_configuration(
    Bucket="my-bucket",
    LifecycleConfiguration={
        "Rules": [
            {
                "ID": "backups",
                "Filter": {"Prefix": "backups/"},
                "Status": "Enabled",
                "NoncurrentVersionExpiration": {"NoncurrentDays": 30},
                "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7},
            },
            {
                "ID": "tmp",
                "Filter": {"Prefix": "tmp/"},
                "Status": "Enabled",
                "Expiration": {"Days": 1},
                "NoncurrentVersionExpiration": {"NoncurrentDays": 1},
                "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1},
            },
        ]
    },
)

for rule in s3.get_bucket_lifecycle_configuration(Bucket="my-bucket")["Rules"]:
    print(rule["ID"], rule["Filter"])
```

**JavaScript (AWS SDK v3)**

```js title="lifecycle.mjs"
import {
  GetBucketLifecycleConfigurationCommand,
  PutBucketLifecycleConfigurationCommand,
  S3Client,
} from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

await s3.send(
  new PutBucketLifecycleConfigurationCommand({
    Bucket: "my-bucket",
    LifecycleConfiguration: {
      Rules: [
        {
          ID: "backups",
          Filter: { Prefix: "backups/" },
          Status: "Enabled",
          NoncurrentVersionExpiration: { NoncurrentDays: 30 },
          AbortIncompleteMultipartUpload: { DaysAfterInitiation: 7 },
        },
        {
          ID: "tmp",
          Filter: { Prefix: "tmp/" },
          Status: "Enabled",
          Expiration: { Days: 1 },
          NoncurrentVersionExpiration: { NoncurrentDays: 1 },
          AbortIncompleteMultipartUpload: { DaysAfterInitiation: 1 },
        },
      ],
    },
  }),
);

const { Rules } = await s3.send(
  new GetBucketLifecycleConfigurationCommand({ Bucket: "my-bucket" }),
);
console.log(Rules);
```

`DeleteBucketLifecycle` removes every rule. A bucket with no rules answers
`GetBucketLifecycleConfiguration` with `404 NoSuchLifecycleConfiguration`, as Amazon S3 does.

### Rule IDs

A client that keeps track of its rules by `ID` finds them where it left them:

- A rule keeps the `ID` you gave it, including when the rules are later saved from the
  dashboard.
- A rule without an `ID`, or added in the dashboard, gets one generated from its prefix,
  starting with `strg-`.
- **A rule with `Expiration` reads back as two rules.** STRG adds a companion rule on the
  same prefix carrying `ExpiredObjectDeleteMarker`, with the ID `<ID>_marker` - for the example
  above, `tmp_marker`. There is no need to send it yourself, and a configuration you read back
  can be sent again unchanged: a companion rule is welcome beside an `Expiration` with `Days` on
  the same prefix, and is replaced by the generated one.

### What is supported

A rule holds a `Filter` with a `Prefix`, `Status` `Enabled`, and the three actions above. For
anything beyond that, STRG answers `501 NotImplemented` and leaves the bucket's rules exactly
as they were, so a client knows straight away:

| In the configuration | Answer |
| --- | --- |
| `Transition`, `NoncurrentVersionTransition` | `501 NotImplemented` |
| `Expiration` with `Date` instead of `Days` | `501 NotImplemented` |
| `NewerNoncurrentVersions` | `501 NotImplemented` |
| A `Filter` with `Tag`, `And`, `ObjectSizeGreaterThan` or `ObjectSizeLessThan` | `501 NotImplemented` |
| `Status` `Disabled` | `501 NotImplemented`. A rule that is not wanted can simply be removed |
| A number of days that is not a whole number of at least 1 | `501 NotImplemented` |
| An `ExpiredObjectDeleteMarker` rule without an `Expiration` in `Days` on the same prefix, or `ExpiredObjectDeleteMarker` `false` | `501 NotImplemented` |
| A top-level `Prefix` in a `Rule` - the deprecated `PutBucketLifecycle` form | `501 NotImplemented`. `Filter` / `Prefix` carries the same prefix |

### Errors

When a configuration cannot be saved, nothing on the bucket changes and the answer says why:

| Error | When |
| --- | --- |
| `400 InvalidRequest` | No `Content-MD5` or `x-amz-checksum-*` header; or the rules break a [limit](https://docs.strg.com/lifecycle-policies#limits) - overlapping prefixes, two rules for one prefix, more than 100 rules. The message names the problem |
| `400 InvalidArgument` | A rule ID is longer than 255 characters, `_marker` included, or two rules share one |
| `400 MalformedXML` | The body is not a `LifecycleConfiguration`, has no rule with an action, or contains comments, CDATA sections, a DOCTYPE or namespace prefixes |
| `403 AccessDenied` | The access key lacks the permission for the operation |
| `501 NotImplemented` | A feature listed under [What is supported](https://docs.strg.com/lifecycle-policies#what-is-supported) |

## Recipes

A few rules that come up again and again. Each recipe is one rule. A rule for the whole bucket
must be the only rule, so recipes combine by putting all their actions in one whole-bucket rule,
or by giving each its own prefix.

### Keeping 30 days of old versions

A good fit for backups and for anything people edit: overwritten and deleted objects stay
recoverable for 30 days, then their previous versions are deleted. Current versions are never
touched.

| Prefix | Expire current | Delete previous versions | Abort uploads |
| --- | --- | --- | --- |
| *(empty)* | - | 30 days | - |

```json
{ "ID": "keep-30-days", "Filter": { "Prefix": "" }, "Status": "Enabled",
  "NoncurrentVersionExpiration": { "NoncurrentDays": 30 } }
```

### Cleaning up abandoned multipart uploads

Parts of uploads that were never completed are stored and billed, and an upload interrupted by a
dropped connection or a stopped process can leave some behind. This rule aborts any upload still
incomplete a week after it started.

| Prefix | Expire current | Delete previous versions | Abort uploads |
| --- | --- | --- | --- |
| *(empty)* | - | - | 7 days |

```json
{ "ID": "abort-uploads", "Filter": { "Prefix": "" }, "Status": "Enabled",
  "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 } }
```

Both recipes together, as the bucket's only rule:

```json
{ "ID": "whole-bucket", "Filter": { "Prefix": "" }, "Status": "Enabled",
  "NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
  "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 } }
```

### Expiring a `tmp/` prefix

For scratch space - build artefacts, an agent's working files, uploads waiting to be processed:
everything under `tmp/` disappears from listings a day after upload and is deleted a day later.
Expiring the current version alone would keep it billed as a previous version; the second action
is what deletes it.

| Prefix | Expire current | Delete previous versions | Abort uploads |
| --- | --- | --- | --- |
| `tmp/` | 1 day | 1 day | 1 day |

```json
{ "ID": "tmp", "Filter": { "Prefix": "tmp/" }, "Status": "Enabled",
  "Expiration": { "Days": 1 },
  "NoncurrentVersionExpiration": { "NoncurrentDays": 1 },
  "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 } }
```

Each JSON rule goes in the `Rules` array of `lifecycle.json` in the
[AWS CLI example](https://docs.strg.com/lifecycle-policies#through-the-s3-api).
