S3 Lifecycle Policies
Data has a life. A log file is hot the day it’s written and cold a month later. A draft uploaded by a user might never be touched again. Paying full-price Amazon S3 (Simple Storage Service — AWS’s object storage) prices to keep cold data on hot storage forever is one of the most common ways cloud bills quietly grow. An S3 lifecycle policy is a set of rules that automatically moves objects to cheaper storage classes or deletes them after a number of days, with zero code and zero ongoing effort once it’s set up.
What a lifecycle policy actually does
A lifecycle policy is attached to a bucket and contains one or more rules. Each rule does one or both of these things:
- Transition — move an object to a cheaper storage class after it has aged a set number of days (for example, move to Glacier after 90 days).
- Expiration — permanently delete an object after a set number of days.
Rules can apply to the whole bucket or be narrowed by an object key prefix (a folder-like path such as logs/) or by an object tag (a key-value label you attach to objects). This lets you treat logs/ differently from invoices/ in the same bucket.
When to use this (and when not to)
Use lifecycle rules when you have predictable, time-based access patterns: logs, backups, raw data dumps, old user uploads. Use them to enforce data-retention rules (delete after 7 years for compliance) automatically.
Do not use rules to micro-optimise objects whose access pattern is unpredictable — for that, use S3 Intelligent-Tiering, a storage class that moves objects between tiers automatically based on real access, with no per-transition charge. Lifecycle rules are for known schedules; Intelligent-Tiering is for unknown ones.
The cost gotchas you must know first
Lifecycle transitions are not free, and beginners often lose money on them.
Cost warning: Every object transition is billed as a request (roughly $0.01 per 1,000 transition requests to Glacier-class storage in 2026). If you transition millions of tiny objects, the one-time transition cost plus the per-object overhead of the colder class can cost more than just leaving them where they are.
Two hard rules enforced by S3:
- Minimum object size for auto-tiering benefit. Objects smaller than 128 KB are not transitioned to Intelligent-Tiering’s deeper tiers, and small objects in Glacier classes carry a per-object metadata overhead (about 8 KB charged at S3 Standard rates plus 32 KB in Glacier). A 2 KB file in Glacier can cost more than in Standard.
- Minimum age before transition. You generally cannot transition an object to Standard-IA or One Zone-IA until it is at least 30 days old. S3 ignores or rejects shorter windows.
Versioning trap: If the bucket has versioning enabled, deleting or overwriting an object does not free space — it creates a noncurrent version that you keep paying for forever. You must add a
NoncurrentVersionExpirationrule, or your bill will climb silently. See S3 versioning.
Storage class transition targets
| Storage class | Best for | Min storage duration | Retrieval |
|---|---|---|---|
| Standard-IA (Infrequent Access) | Data accessed monthly | 30 days | Instant |
| One Zone-IA | Re-creatable data, one AZ | 30 days | Instant |
| Glacier Instant Retrieval | Archives needing instant reads | 90 days | Instant |
| Glacier Flexible Retrieval | Archives, minutes-to-hours reads | 90 days | Minutes–hours |
| Glacier Deep Archive | Long-term cold storage | 180 days | Up to 12 hours |
A common, sensible chain: Standard → Standard-IA at 30 days → Glacier Flexible at 90 days → expire at 365 days.
Create a rule in the AWS Management Console
- Open the S3 console and click your bucket name.
- Go to the Management tab.
- Under Lifecycle rules, click Create lifecycle rule.
- Give it a name like
archive-and-expire-logs. - Under Choose a rule scope, pick Limit the scope and enter a prefix such as
logs/(or pick Apply to all objects and tick the acknowledgement box). - Under Lifecycle rule actions, tick: Transition current versions of objects, Expire current versions of objects, and Permanently delete noncurrent versions (if versioning is on).
- Add a transition: Standard-IA after 30 days; add another: Glacier Flexible Retrieval after 90 days.
- Set Expire current versions to 365 days.
- Set Delete noncurrent versions to 30 days after they become noncurrent.
- Review the summary timeline and click Create rule.
Create the same rule with the AWS CLI
First write the rule as JSON. This targets the logs/ prefix, transitions and expires current objects, and cleans up noncurrent versions.
{
"Rules": [
{
"ID": "archive-and-expire-logs",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER" }
],
"Expiration": { "Days": 365 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 30 }
}
]
}
Apply it (AWS CLI v2):
aws s3api put-bucket-lifecycle-configuration \
--bucket my-app-logs-bucket \
--lifecycle-configuration file://lifecycle.json
This command produces no output on success. Read it back to confirm:
aws s3api get-bucket-lifecycle-configuration \
--bucket my-app-logs-bucket
Output:
{
"Rules": [
{
"ID": "archive-and-expire-logs",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER" }
],
"Expiration": { "Days": 365 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 30 }
}
]
}
To scope by tag instead of prefix, replace the Filter with { "Tag": { "Key": "archive", "Value": "true" } }.
Defining it as Infrastructure as Code
If you manage buckets with Terraform, declare the lifecycle inline so it lives in version control:
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "archive-and-expire-logs"
status = "Enabled"
filter { prefix = "logs/" }
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration { days = 365 }
noncurrent_version_expiration { noncurrent_days = 30 }
}
}
Best Practices
- Always pair versioning with a
NoncurrentVersionExpirationrule so old versions cannot accumulate forever. - Skip transitions for buckets full of tiny (under 128 KB) objects — the per-object overhead and request charges usually outweigh the storage savings.
- Use prefixes or tags to apply different schedules within one bucket instead of creating many buckets.
- Add an
AbortIncompleteMultipartUploadaction (7 days is typical) so failed uploads do not leave invisible billable fragments. - Respect the minimum durations: 30 days for IA classes, 90 for Glacier instant/flexible, 180 for Deep Archive — you pay the full minimum even if you delete sooner.
- For unpredictable access, prefer Intelligent-Tiering over hand-written transition rules.
- Review the rule’s timeline summary in the console before saving — it shows exactly when each object will move or be deleted.