# PutBucketLifecycleConfiguration

PutBucketLifecycleConfiguration sets a bucket's lifecycle configuration: the rules that expire
current object versions, delete noncurrent versions and abort incomplete multipart uploads. The
request replaces the bucket's whole configuration, rules set in the dashboard included - the
dashboard shows and edits the same rules. The deprecated
[PutBucketLifecycle](https://docs.strg.com/s3-api/PutBucketLifecycle) sends exactly the same request, so this operation
is the better choice. [Lifecycle policies](https://docs.strg.com/lifecycle-policies) explains what the rules do.

**Info: Good to know on STRG**

- Rules hold the three actions STRG offers - `Expiration` with `Days`,
  `NoncurrentVersionExpiration` with `NoncurrentDays` and `AbortIncompleteMultipartUpload` -
  each filtered by key prefix. STRG answers anything else with `501 NotImplemented`, so a
  client knows straight away.
- Each prefix has one rule, which carries all of its actions. Prefixes stay apart - no prefix
  starts with another - and a rule for the whole bucket is the bucket's one rule, so it is always
  clear which rule applies to an object.
- A bucket holds up to 100 rules.
- Every rule with `Expiration` gets a companion rule from STRG that tidies away expired
  delete markers, and [GetBucketLifecycleConfiguration](https://docs.strg.com/s3-api/GetBucketLifecycleConfiguration)
  returns it.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check, and
  `x-amz-transition-default-minimum-object-size` is set aside, as it concerns storage-class
  transitions.

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

`NoncurrentVersionExpiration` deletes object versions permanently; they cannot be restored.
Every bucket keeps versions, so a rule with `Expiration` alone frees no storage: an expired object
stays on as a noncurrent version, billed as stored data, until a `NoncurrentVersionExpiration`
rule removes it or it is deleted.
[Keeping 30 days of old versions](https://docs.strg.com/lifecycle-policies#keeping-30-days-of-old-versions)
shows a rule that does both.

## When you'd use it

- **Keeping storage costs in check.** Every version of every object is billed as stored data, so
  a team whose documents change daily keeps the last 30 days of old versions and lets STRG
  delete the rest - the
  [Keeping 30 days of old versions](https://docs.strg.com/lifecycle-policies#keeping-30-days-of-old-versions)
  recipe.
- **Tidying up after interrupted uploads.** A mobile app uploading large videos in parts leaves
  unfinished uploads behind whenever a phone loses its connection, and their parts are billed
  too. A rule with `AbortIncompleteMultipartUpload` clears them away after a day -
  [Cleaning up abandoned multipart uploads](https://docs.strg.com/lifecycle-policies#cleaning-up-abandoned-multipart-uploads).
- **Scratch space and logs.** An agent keeping intermediate results under `tmp/`, or a service
  writing logs under `logs/`, has that data expire on its own after a few days -
  [Expiring a `tmp/` prefix](https://docs.strg.com/lifecycle-policies#expiring-a-tmp-prefix).
- **Settings kept in code.** A deploy script or infrastructure tool that manages each bucket's
  settings writes the rules from the repository, so what the repository says is what the bucket
  does.

## Request

```http
PUT /?lifecycle HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: <SHA-256 of the body>
Content-MD5: <base64 MD5 of the body>
Content-Type: application/xml

<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Rule>
    <ID>expire-logs</ID>
    <Filter>
      <Prefix>logs/</Prefix>
    </Filter>
    <Status>Enabled</Status>
    <Expiration>
      <Days>30</Days>
    </Expiration>
    <NoncurrentVersionExpiration>
      <NoncurrentDays>7</NoncurrentDays>
    </NoncurrentVersionExpiration>
  </Rule>
</LifecycleConfiguration>
```

Path-style, the same request is `PUT /my-bucket?lifecycle` on `s3.eu-central-1.strg.com`.
[Addressing](https://docs.strg.com/s3-api/overview#addressing) in the S3 API overview has both styles.

### Permissions

The access key needs **Manage lifecycle rules** (`bucket-lifecycle:write`). A key limited to
certain buckets works too, as long as this bucket is one of them. A key prefix does not narrow
this operation, because the configuration belongs to the whole bucket.
[Access keys](https://docs.strg.com/access-keys) has every permission.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket whose lifecycle configuration is set. |
| `lifecycle` | Flag | Yes | Selects the lifecycle subresource. Sent without a value. |

STRG answers `501 NotImplemented` to any other query parameter, so a client knows straight
away. The `X-Amz-*` parameters of a presigned URL and the `x-id` parameter the JavaScript SDK
adds are welcome.

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-MD5` | String | One integrity header is required | The base64 MD5 digest of the body. STRG checks it and answers `400 BadDigest` if it does not match. |
| `x-amz-checksum-crc32`, `x-amz-checksum-crc32c`, `x-amz-checksum-crc64nvme`, `x-amz-checksum-sha1`, `x-amz-checksum-sha256` | String | One integrity header is required | A checksum of the body, which works in place of `Content-MD5`. The AWS SDKs and CLI send one by default. |
| `x-amz-sdk-checksum-algorithm` | String | No | The algorithm of the `x-amz-checksum-*` header the SDK sends. Accepted. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |
| `x-amz-transition-default-minimum-object-size` | String | No | Set aside. It concerns storage-class transitions, and STRG has one storage class. |

Every request carries either `Content-MD5` or an `x-amz-checksum-*` header, and one with neither
gets `400 InvalidRequest`. The AWS CLI and SDKs add one automatically.

The body arrives whole. STRG answers a streaming `aws-chunked` upload of the body with
`501 NotImplemented`; the AWS CLI and SDKs never send one for this operation.

### Request body

A `LifecycleConfiguration` element holding up to 100 `Rule` elements, at most 2 MiB. The document
is plain XML: a comment, CDATA section, `DOCTYPE`, processing instruction, namespace prefix or
attribute other than `xmlns` gets `400 MalformedXML`. A configuration with no rules is
`400 MalformedXML` too - removing every rule is the work of
[DeleteBucketLifecycle](https://docs.strg.com/s3-api/DeleteBucketLifecycle).

| Element | Required | Description |
| --- | --- | --- |
| `Rule` | Yes | One lifecycle rule. Up to 100, not counting companion rules. |
| `Rule/ID` | No | The rule's ID, at most 255 characters, unique within the configuration. For a rule with `Expiration/Days`, the ID plus the `_marker` suffix of its companion rule also fits in 255 characters and stays clear of every other ID. A rule sent without one gets an ID generated by STRG. The ID is kept, including across a save in the dashboard. |
| `Rule/Status` | Yes | `Enabled`. STRG answers `Disabled` with `501 NotImplemented`; a rule that should stop applying is simply left out of the configuration. |
| `Rule/Filter` | No | The objects the rule applies to. An empty or missing `Filter` means every object in the bucket. |
| `Rule/Filter/Prefix` | No | The key prefix the rule applies to, at most 1,024 bytes, without control characters. It does not overlap another rule's prefix - one prefix never starts with another. |
| `Rule/Filter/Tag` | No | STRG answers `501 NotImplemented`: rules here filter by key prefix. |
| `Rule/Filter/ObjectSizeGreaterThan`, `Rule/Filter/ObjectSizeLessThan` | No | STRG answers `501 NotImplemented`: rules here filter by key prefix. |
| `Rule/Filter/And` | No | STRG answers `501 NotImplemented`: rules here filter by key prefix. |
| `Rule/Prefix` | No | The deprecated top-level prefix. STRG answers `501 NotImplemented`; `Filter/Prefix` sets the same thing. |
| `Rule/Expiration/Days` | No | Days after an object is created when its current version becomes a noncurrent version (a delete marker is placed on top of it). A whole number of at least 1. |
| `Rule/Expiration/Date` | No | STRG answers `501 NotImplemented`; `Days` sets an expiration. |
| `Rule/Expiration/ExpiredObjectDeleteMarker` | No | `true` is accepted in a rule on the same prefix as a rule with `Expiration/Days`, and is then replaced by the companion rule STRG adds - its ID is not kept. Anywhere else, or `false`, STRG answers `501 NotImplemented`. |
| `Rule/NoncurrentVersionExpiration/NoncurrentDays` | No | Days after a version becomes noncurrent when it is permanently deleted. A whole number of at least 1. |
| `Rule/NoncurrentVersionExpiration/NewerNoncurrentVersions` | No | STRG answers `501 NotImplemented`. |
| `Rule/AbortIncompleteMultipartUpload/DaysAfterInitiation` | No | Days after a multipart upload starts when it is aborted if not completed. A whole number of at least 1. |
| `Rule/Transition` | No | STRG answers `501 NotImplemented`: there is one storage class, so nothing to move objects between. |
| `Rule/NoncurrentVersionTransition` | No | STRG answers `501 NotImplemented`, for the same reason. |

A day count that is not a whole number of at least 1 gets `501 NotImplemented`. A rule with no
action at all is set aside.

**The companion rule.** For every rule with `Expiration/Days`, STRG stores a second rule on
the same prefix, with the ID `<ID>_marker` and `Expiration/ExpiredObjectDeleteMarker` set to
`true`, which removes a delete marker once no versions remain beneath it. There is no need to send
it. [GetBucketLifecycleConfiguration](https://docs.strg.com/s3-api/GetBucketLifecycleConfiguration) returns it, and a
configuration read from there can be sent back unchanged.

## Response

A successful request answers `200 OK` with an empty body once the configuration is stored. The
new rules replace the old ones.

### Response headers

| Name | Description |
| --- | --- |
| `x-amz-request-id` | The request ID, useful in a support request. |
| `x-amz-transition-default-minimum-object-size` | Relates to storage-class transitions, which STRG has no use for, so this header carries no meaning for a STRG bucket. |

### Response body

None.

## Errors

These are the answers a client can get instead of `200`, and the situation behind each.

| Code | HTTP status | When |
| --- | --- | --- |
| `MalformedXML` | 400 | The body is not well-formed XML, its root is not `LifecycleConfiguration`, it uses a comment, CDATA section, `DOCTYPE`, processing instruction, namespace prefix or attribute other than `xmlns`, or it holds no rule with an action. |
| `InvalidRequest` | 400 | The request carries neither `Content-MD5` nor an `x-amz-checksum-*` header. Or the rules go past a limit: more than 100, two rules for one prefix or overlapping prefixes, a whole-bucket rule beside another rule, a prefix over 1,024 bytes, or a control character in a prefix or ID. The message names the problem. |
| `InvalidArgument` | 400 | A rule ID is longer than 255 characters (with `_marker`, for a rule with `Expiration/Days`), or two rules share an ID. |
| `BadDigest` | 400 | `Content-MD5` does not match the body. |
| `XAmzContentSHA256Mismatch` | 400 | `x-amz-content-sha256` does not match the body. |
| `MaxMessageLengthExceeded` | 400 | The body is larger than 2 MiB. |
| `NotImplemented` | 501 | The configuration uses an element STRG does not take (see *Request body*), the request carries a query parameter this operation does not take, or it sends the body as a streaming `aws-chunked` upload. |
| `NoSuchBucket` | 404 | The bucket does not exist, it is in a region other than the endpoint's, or the account stores nothing in this endpoint's region yet. |
| `AccessDenied` | 403 | The access key does not hold **Manage lifecycle rules**, the bucket is outside its scope, or the account is not active. |
| `InvalidAccessKeyId` | 403 | The access key does not exist, has expired or has been revoked - or it was created moments ago and is not yet usable in this region, where the account already stores data. A retry a moment later succeeds. |
| `SignatureDoesNotMatch` | 403 | The request signature does not match. |
| `RequestTimeTooSkewed` | 403 | The request time is more than 15 minutes from the server's. |
| `AuthorizationHeaderMalformed` | 400 | The signature's credential scope names a region other than the endpoint's. |
| `SlowDown` | 503 | Too many requests at once. A retry after the `Retry-After` interval goes through. |
| `ServiceUnavailable` | 503 | A temporary failure. A retry of the same request is safe. |

## Differences from Amazon S3

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Actions | Expiration by days or date, transitions, noncurrent expiration and transitions, `NewerNoncurrentVersions`, abort incomplete uploads | `Expiration/Days`, `NoncurrentVersionExpiration/NoncurrentDays` and `AbortIncompleteMultipartUpload` - three actions that cover expiring data, old versions and abandoned uploads. The rest gets `501 NotImplemented` |
| Filters | Prefix, tags, object size, `And` | By key prefix |
| Rules per prefix | Any number, overlapping allowed | One rule per prefix, holding all its actions, and prefixes that never overlap; a whole-bucket rule is the bucket's one rule. It is always clear which rule applies to an object |
| Rule count | 1,000 | 100, not counting companion rules |
| Disabled rules | Stored with `Status` `Disabled` | `501 NotImplemented`. A rule that should stop applying is left out of the configuration |
| Expiration rules | Stored as sent | A companion `<ID>_marker` rule with `ExpiredObjectDeleteMarker` is added to every rule with `Expiration/Days`, so expired delete markers are tidied away without asking; a client's own `ExpiredObjectDeleteMarker` rule is replaced by it |
| Top-level `Prefix` | Accepted for backward compatibility | `501 NotImplemented`. `Filter/Prefix` sets the same thing |
| Versioning | Rules apply to unversioned or versioned buckets | Every bucket keeps versions, so `Expiration` leaves a noncurrent version behind; a `NoncurrentVersionExpiration` action beside it frees the storage |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`, also when the account stores nothing in the endpoint's region. The bucket's own regional endpoint reaches it |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check: a key only reaches its own account's buckets |

## Examples

Expiring objects under `logs/` after 30 days, deleting their noncurrent versions 7 days later,
and aborting multipart uploads under `uploads/` that are not completed within a day:

**AWS CLI**

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

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

**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": "expire-logs",
                "Filter": {"Prefix": "logs/"},
                "Status": "Enabled",
                "Expiration": {"Days": 30},
                "NoncurrentVersionExpiration": {"NoncurrentDays": 7},
            },
            {
                "ID": "abort-uploads",
                "Filter": {"Prefix": "uploads/"},
                "Status": "Enabled",
                "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1},
            },
        ]
    },
)
```

**JavaScript (AWS SDK v3)**

```javascript
import { PutBucketLifecycleConfigurationCommand, S3Client } from "@aws-sdk/client-s3";

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

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

Adding a rule to an existing configuration means reading the current rules first, appending the
new one and sending the whole list back. The companion rules in what was read can go back as they
are:

**AWS CLI**

```bash
aws s3api get-bucket-lifecycle-configuration \
  --bucket my-bucket \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1 \
  --query '{Rules: Rules}' > lifecycle.json
```

With the new rule added to `lifecycle.json`, the same `put-bucket-lifecycle-configuration`
command as above sends it.

**Python (boto3)**

```python
import boto3

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

try:
    rules = s3.get_bucket_lifecycle_configuration(Bucket="my-bucket")["Rules"]
except s3.exceptions.ClientError as error:
    if error.response["Error"]["Code"] != "NoSuchLifecycleConfiguration":
        raise
    rules = []

rules.append(
    {
        "ID": "delete-old-versions",
        "Filter": {"Prefix": "documents/"},
        "Status": "Enabled",
        "NoncurrentVersionExpiration": {"NoncurrentDays": 90},
    }
)

s3.put_bucket_lifecycle_configuration(
    Bucket="my-bucket",
    LifecycleConfiguration={"Rules": rules},
)
```

**JavaScript (AWS SDK v3)**

```javascript
import {
  GetBucketLifecycleConfigurationCommand,
  PutBucketLifecycleConfigurationCommand,
  S3Client,
} from "@aws-sdk/client-s3";

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

let rules = [];
try {
  const output = await client.send(
    new GetBucketLifecycleConfigurationCommand({ Bucket: "my-bucket" }),
  );
  rules = output.Rules ?? [];
} catch (error) {
  if (error.name !== "NoSuchLifecycleConfiguration") throw error;
}

rules.push({
  ID: "delete-old-versions",
  Filter: { Prefix: "documents/" },
  Status: "Enabled",
  NoncurrentVersionExpiration: { NoncurrentDays: 90 },
});

await client.send(
  new PutBucketLifecycleConfigurationCommand({
    Bucket: "my-bucket",
    LifecycleConfiguration: { Rules: rules },
  }),
);
```

The new rule's prefix stays clear of every existing one. A prefix that overlaps gets
`400 InvalidRequest`, and the configuration stays as it was.

## Related

- [GetBucketLifecycleConfiguration](https://docs.strg.com/s3-api/GetBucketLifecycleConfiguration)
- [DeleteBucketLifecycle](https://docs.strg.com/s3-api/DeleteBucketLifecycle)
- [PutBucketLifecycle](https://docs.strg.com/s3-api/PutBucketLifecycle) (deprecated)
- [Lifecycle policies](https://docs.strg.com/lifecycle-policies)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
