Introduction
S3 lifecycle rules automatically transition objects between storage classes and delete them after specified periods. When rules don't work as expected, objects remain in expensive storage tiers past their expiration date, increasing costs and potentially violating data retention policies.
Symptoms
Objects not deleted:
```bash $ aws s3 ls s3://my-bucket/logs/ --recursive | head
2024-01-01 10:00:00 logs/app-2024-01-01.log # Should be deleted after 30 days 2024-02-01 10:00:00 logs/app-2024-02-01.log # Should be deleted after 30 days # Objects older than 30 days still present ```
Storage class not transitioned:
```bash $ aws s3api head-object --bucket my-bucket --key old-file \ --query 'StorageClass'
"STANDARD" # Should be GLACIER or IA after transition period ```
Lifecycle rule exists but not working:
```bash $ aws s3api get-bucket-lifecycle-configuration --bucket my-bucket
# Shows rules, but objects not transitioning/deleting ```
Common Causes
- 1.Rule status disabled - Rule created but not enabled
- 2.Filter mismatch - Object prefix doesn't match rule filter
- 3.Incomplete multipart uploads - Different handling for uploads
- 4.Versioning enabled - Rules apply differently to versions
- 5.Minimum storage duration - Objects can't transition before minimum period
- 6.Rule timing - S3 processes rules once daily
- 7.Wrong action type - Using transition instead of expiration
Step-by-Step Fix
- 1.Check logs for specific error messages
- 2.Verify configuration settings
- 3.Test network connectivity
- 4.Review recent changes
- 5.Apply corrective action
- 6.Verify the fix
Step 1: Check Lifecycle Configuration
```bash # Get all lifecycle rules aws s3api get-bucket-lifecycle-configuration --bucket my-bucket \ --query 'Rules[*].[ID,Status,Filter,Expiration,Transitions]'
# Check rule status - must be "Enabled" # Filter must match your object prefixes ```
Step 2: Verify Rule is Enabled
bash
# If rule status is "Disabled"
aws s3api get-bucket-lifecycle-configuration --bucket my-bucket \
--query 'Rules[?Status==Disabled`].ID'
# Enable the rule aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \ --lifecycle-configuration '{ "Rules": [{ "ID": "expire-logs", "Status": "Enabled", "Filter": {"Prefix": "logs/"}, "Expiration": {"Days": 30} }] }' ```
Step 3: Check Object Prefix Matches Filter
```bash # If rule has prefix filter aws s3api get-bucket-lifecycle-configuration --bucket my-bucket \ --query 'Rules[*].Filter.Prefix'
# Objects must match prefix exactly # Rule with prefix "logs/" only affects objects like: # logs/app.log - matches # logs/2024/app.log - matches # app.log - does NOT match # backup/logs/app.log - does NOT match
# Check object key aws s3 ls s3://my-bucket/ --recursive
# Use empty filter for all objects aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \ --lifecycle-configuration '{ "Rules": [{ "ID": "expire-all", "Status": "Enabled", "Filter": {}, "Expiration": {"Days": 90} }] }' ```
Step 4: Handle Versioned Buckets
```bash # Check if versioning is enabled aws s3api get-bucket-versioning --bucket my-bucket
# For versioned buckets, use NonCurrentVersionExpiration aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \ --lifecycle-configuration '{ "Rules": [{ "ID": "expire-versions", "Status": "Enabled", "Filter": {"Prefix": "logs/"}, "Expiration": {"Days": 30}, "NonCurrentVersionExpiration": {"NonCurrentDays": 30} }] }'
# Expiration affects current version only # NonCurrentVersionExpiration affects old versions ```
Step 5: Check Transition Minimum Duration
```bash # Storage class minimum durations: # STANDARD_IA: 30 days minimum # ONEZONE_IA: 30 days minimum # GLACIER: 90 days minimum (formerly 30) # DEEP_ARCHIVE: 180 days minimum
# Rule transitioning to Glacier after 30 days: # Wrong: Transition to Glacier after 30 days violates minimum # Correct: First transition to IA (30+ days), then Glacier (additional 60+ days)
aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \ --lifecycle-configuration '{ "Rules": [{ "ID": "archive-policy", "Status": "Enabled", "Filter": {"Prefix": "archive/"}, "Transitions": [ {"Days": 30, "StorageClass": "STANDARD_IA"}, {"Days": 90, "StorageClass": "GLACIER"} ], "Expiration": {"Days": 365} }] }' ```
Step 6: Handle Multipart Uploads
```bash # Incomplete multipart uploads consume storage # Add AbortIncompleteMultipartUpload action
aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \ --lifecycle-configuration '{ "Rules": [{ "ID": "cleanup-uploads", "Status": "Enabled", "Filter": {"Prefix": "uploads/"}, "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7} }] }'
# Check for incomplete uploads aws s3api list-multipart-uploads --bucket my-bucket ```
Step 7: Understand Rule Processing Timing
```bash # S3 lifecycle rules processed once daily # Not immediate - may take up to 24 hours
# Check when rules were last processed (via metrics) aws cloudwatch get-metric-statistics \ --namespace AWS/S3 \ --metric-name ObjectCount \ --dimensions Name=BucketName,Value=my-bucket,Name=StorageType,Value=StandardStorage \ --statistics Average \ --period 86400
# Count should decrease after rule runs ```
Step 8: Verify Object Has Required Days
```bash # Check object age aws s3api head-object --bucket my-bucket --key old-file \ --query '{LastModified:LastModified,StorageClass:StorageClass}'
# If object created 5 days ago, rule with 30-day expiration won't affect it yet
# Get all objects with creation date aws s3api list-objects-v2 --bucket my-bucket \ --query 'Contents[*].[Key,LastModified]' ```
Step 9: Check for Size-Based Filters
```bash # Rules can have size-based filters aws s3api get-bucket-lifecycle-configuration --bucket my-bucket \ --query 'Rules[*].Filter.ObjectSizeGreaterThan'
# If size filter exists, only objects meeting size criteria affected # Example: ObjectSizeGreaterThan: 100KB # Objects smaller than 100KB won't be processed
# Remove size filter if not needed aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \ --lifecycle-configuration '{ "Rules": [{ "ID": "expire-all-sizes", "Status": "Enabled", "Filter": {"Prefix": "logs/"}, "Expiration": {"Days": 30} }] }' ```
Step 10: Use Multiple Rules for Different Prefixes
# Different retention for different data
aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \
--lifecycle-configuration '{
"Rules": [
{
"ID": "expire-logs",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Expiration": {"Days": 30}
},
{
"ID": "expire-temp",
"Status": "Enabled",
"Filter": {"Prefix": "temp/"},
"Expiration": {"Days": 7}
},
{
"ID": "archive-docs",
"Status": "Enabled",
"Filter": {"Prefix": "docs/"},
"Transitions": [{"Days": 90, "StorageClass": "GLACIER"}]
}
]
}'S3 Lifecycle Rule Processing Timeline
| Action | Timing |
|---|---|
| Rule evaluation | Daily (UTC midnight) |
| Transition execution | Within 24 hours |
| Expiration execution | Within 24 hours |
| Delete marker removal | Within 24 hours |
| Incomplete upload abort | After specified days |
Verification
```bash # After fixing rule, upload test object aws s3 cp test.txt s3://my-bucket/logs/test.txt
# Wait 24+ hours for rule to process # For immediate verification of rule config: aws s3api get-bucket-lifecycle-configuration --bucket my-bucket \ --query 'Rules[*].[ID,Status]'
# Status should be "Enabled"
# Monitor storage metrics aws cloudwatch get-metric-statistics \ --namespace AWS/S3 \ --metric-name BucketSizeBytes \ --dimensions Name=BucketName,Value=my-bucket \ --statistics Average \ --period 86400 ```
Related Issues
- [Fix AWS S3 Replication Not Working](/articles/fix-aws-s3-replication-not-working)
- [Fix AWS S3 Bucket Policy Denied](/articles/fix-aws-s3-bucket-policy-denied)
- [Fix AWS S3 Inventory Report Missing](/articles/fix-aws-s3-inventory-report-missing)
Related Articles
- [AWS troubleshooting: Fix IAM Permission Denied - Complete Tro](fix-iam-permission-denied)
- [AWS cloud troubleshooting: AWS ACM Certificate Pending Validation Because the](aws-acm-certificate-pending-validation-wrong-route53-zone)
- [AWS cloud troubleshooting: AWS ALB Returns 502 Because the Target Closed the ](aws-alb-502-target-closed-connection-keepalive-timeout-mismatch)
- [AWS cloud troubleshooting: Fix AWS ALB CreateListener TargetGroupNotFound Err](aws-alb-createlistener-targetgroupnotfound)
- [AWS cloud troubleshooting: Fix Aws Alb Lambda 502 Bad Gateway Issue in AWS](aws-alb-lambda-502-bad-gateway)
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Fix AWS S3 Lifecycle Rule Not Expiring Objects", "description": "Troubleshoot S3 lifecycle rule issues. Fix expiration configuration, versioning settings, and rule filters.", "url": "https://www.fixwikihub.com/fix-aws-s3-lifecycle-rule-not-expiring", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-04-01T12:13:20.528Z", "dateModified": "2026-04-01T12:13:20.528Z" } </script>