Lifecycle Rules¶
A lifecycle rule expires objects on a schedule. Rules are per bucket, matched by key prefix, and run by a background worker.
What a rule can do¶
Two independent actions, and a rule needs at least one:
| Action | Applies to | Effect |
|---|---|---|
expiration |
Current objects older than N days | Deletes the object |
noncurrent_version_expiration |
Non-current versions older than N days | Permanently removes that version |
N is whole days, between 1 and 36500.
A rule with neither action is refused. A rule that expires nothing would sit there looking like cleanup was happening when it was not.
Deletion semantics¶
The two actions behave differently, and the difference matters:
expirationperforms an ordinary delete. On a versioned bucket that writes a delete marker and the history is retained. On an unversioned bucket the object is gone.noncurrent_version_expirationremoves a specific version permanently. It never touches the current version.
So on a versioned bucket, expiration alone reclaims no space. Pair the two to
actually shrink the bucket:
flowchart LR
A[Current object, 30 days old] -->|expiration: 30| B[Delete marker written]
B --> C[Previous version becomes non-current]
C -->|noncurrent_version_expiration: 7| D[Version removed, space reclaimed]
See Versioning.
Creating a rule¶
curl -X POST https://management.example.com/api/v1/buckets/logs/lifecycle \
-H "Authorization: Bearer <your-management-token>" \
-H "Content-Type: application/json" \
-d '{"prefix":"debug/","expiration":30,"noncurrent_version_expiration":7}'
| Field | Required | Default |
|---|---|---|
prefix |
no | "" — the whole bucket |
enabled |
no | true |
expiration |
one of the two | — |
noncurrent_version_expiration |
one of the two | — |
The response is the created rule, including its id.
An empty prefix matches every object in the bucket. That is legitimate for a scratch bucket and a mistake almost everywhere else — check the prefix before creating a rule.
Listing and updating¶
curl https://management.example.com/api/v1/buckets/logs/lifecycle \
-H "Authorization: Bearer <your-management-token>"
Updates are complete replacements, addressed through the bucket:
curl -X PUT https://management.example.com/api/v1/buckets/logs/lifecycle/<rule-id> \
-H "Authorization: Bearer <your-management-token>" \
-H "Content-Type: application/json" \
-d '{"prefix":"debug/","enabled":false,"expiration":7}'
Every field is sent. Omitting noncurrent_version_expiration clears it — that is
deliberate, so "remove this action" is expressible and never ambiguous with "leave it
alone".
Setting enabled to false is the safe way to stop a rule while you think about it.
The rule stays visible and its progress cursor is preserved.
Deleting a rule¶
curl -X DELETE https://management.example.com/api/v1/lifecycle-rules/<rule-id> \
-H "Authorization: Bearer <your-management-token>"
Deleting a rule stops future expiry. Objects already expired are not restored.
How the worker runs¶
flowchart LR
A[Timer fires] --> B{Allowed to scan?}
B -->|no| A
B -->|yes| C[For each enabled rule]
C --> D[Scan one bounded page from the saved cursor]
D --> E[Expire what is past the cutoff]
E --> F[Persist the cursor]
F --> A
Three properties follow from this design:
- Bounded. Each pass scans at most
lifecycle.batch_sizeentries per rule per action. A rule over ten million objects does not stall the deployment. - Restart-safe. The cursor is durable. A restart resumes the scan; it does not start over.
- Single-writer. An activation gate lets one scan run at a time, so a slow pass never overlaps the next one and produces duplicate delete markers.
Tuning:
interval_seconds accepts 1–86400; batch_size accepts 1–1000.
A rule can only expire batch_size entries per pass. With the defaults that is 100
objects an hour — deliberately gentle. Raise batch_size or lower interval_seconds
if a backlog is not draining; do both if it is large.
Timing is approximate¶
An object is expired on the first pass after its age crosses the cutoff, and only when the cursor reaches it. With the default hourly interval, expect expiry within hours of the boundary, not at the moment it is crossed. Lifecycle is a cleanup mechanism, not a deadline enforcer.
For deletion at an exact time, delete explicitly.
Observing it¶
Every expiry is written to the audit log as
lifecycle.expire-object or lifecycle.expire-noncurrent-version, with principal
system:lifecycle:
record-store audit \
--principal system:lifecycle \
--limit 200 \
--endpoint https://management.example.com
Each pass that scanned anything also logs lifecycle scan completed with scanned,
expired, and failure counts. A non-zero failure count means individual deletions
failed; they are retried on the next pass, and the details are in the process log.