This check flags S3 buckets with no active lifecycle rules, which means old objects, expired multipart uploads, and stale versions pile up forever and quietly inflate your bill. Add a lifecycle configuration to transition cold data to cheaper storage classes and expire what you no longer need.
An S3 bucket with no lifecycle rules will hold onto every object you ever put in it, indefinitely, at full Standard storage pricing, unless someone manually intervenes. That is fine for a small bucket. It becomes a real problem when logs, backups, build artifacts, and incomplete uploads accumulate over months and years. Lensix raises s3_no_lifecycle_rules when a bucket has no active lifecycle configuration so you can decide what the right retention and tiering strategy should be.
What this check detects
The check looks at each S3 bucket in your account and inspects its lifecycle configuration. If the bucket has no lifecycle configuration, or has rules that are all disabled, it gets flagged.
You can reproduce the same result with the AWS CLI:
aws s3api get-bucket-lifecycle-configuration --bucket my-bucket
If the bucket has no rules, the call returns an error:
{
"Error": {
"Code": "NoSuchLifecycleConfiguration",
"Message": "The lifecycle configuration does not exist"
}
}
That NoSuchLifecycleConfiguration response is exactly the state this check is concerned with.
Note: A lifecycle rule is a set of actions S3 applies to objects on a schedule. Rules can transition objects between storage classes (for example, Standard to Glacier after 90 days), expire objects after a set age, clean up old object versions, and abort incomplete multipart uploads. Rules run automatically once a day with no compute cost to you.
Why it matters
This is primarily a cost and hygiene issue, but it has security and compliance angles too.
Storage costs grow silently
S3 Standard runs around $0.023 per GB per month. That sounds cheap until a log bucket grows to several terabytes that nobody reads after the first week. Moving cold data to S3 Standard-IA, Glacier Instant Retrieval, or Glacier Deep Archive can cut storage cost by 40 to 95 percent depending on access patterns. Without lifecycle rules, none of that happens.
Incomplete multipart uploads bill you for nothing
When a multipart upload fails partway through and the client never cleans up, the uploaded parts stay in the bucket. They do not show up in the normal object listing, and they keep accruing storage charges. On busy buckets with flaky clients, this can quietly cost real money. A single lifecycle rule fixes it for the whole bucket.
Old versions never go away
If versioning is enabled and you have no lifecycle rule for noncurrent versions, every overwrite and delete keeps the old copy forever. A bucket storing 100 GB of current data might be holding several times that in stale versions.
Retention compliance
Regulations like GDPR and various data retention policies require that you do not keep personal data longer than necessary. Lifecycle expiration is the standard mechanism for enforcing maximum retention on log and event data. No lifecycle rule means no automated enforcement, which auditors will notice.
Warning: Lifecycle rules act on data permanently. An expiration rule deletes objects, and a transition rule changes how quickly and cheaply you can read them back. Glacier Deep Archive retrievals can take up to 12 hours and carry retrieval fees. Match each rule to how the data is actually accessed before you apply it.
How to fix it
Decide what should happen to objects in the bucket, then apply a lifecycle configuration. The pattern below is a reasonable default for a log or backup bucket: transition to infrequent access after 30 days, archive after 90, expire after a year, clean up old versions, and abort stale multipart uploads.
Option 1: AWS CLI
Create a JSON file describing the rules:
{
"Rules": [
{
"ID": "transition-and-expire",
"Status": "Enabled",
"Filter": { "Prefix": "" },
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER" }
],
"Expiration": { "Days": 365 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 60 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
Apply it to the bucket:
aws s3api put-bucket-lifecycle-configuration \
--bucket my-bucket \
--lifecycle-configuration file://lifecycle.json
Confirm it took effect:
aws s3api get-bucket-lifecycle-configuration --bucket my-bucket
Danger: The Expiration and NoncurrentVersionExpiration actions permanently delete objects on a schedule, with no recovery once they run. Test the rule on a non-production bucket first, and double-check the Days values against your real retention requirements before applying to production data.
Option 2: AWS Console
- Open the S3 console and select your bucket.
- Go to the Management tab.
- Under Lifecycle rules, choose Create lifecycle rule.
- Name the rule and choose whether it applies to the whole bucket or to a prefix or tag filter.
- Select the actions you want: transition current versions, transition noncurrent versions, expire current versions, permanently delete noncurrent versions, or delete expired delete markers and incomplete multipart uploads.
- Enter the day thresholds for each action and review the summary.
- Choose Create rule.
Option 3: Terraform
If you manage buckets as code, define the lifecycle configuration alongside the bucket so it is never missed:
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "transition-and-expire"
status = "Enabled"
filter {}
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
noncurrent_version_expiration {
noncurrent_days = 60
}
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
Tip: If you only want one quick win with zero risk of deleting useful data, start with the abort_incomplete_multipart_upload rule on every bucket. It removes orphaned upload parts that are pure waste, and it can never touch a real object.
How to prevent it from happening again
The fix above closes the gap on one bucket. To stop the problem coming back, make lifecycle configuration part of how buckets get created.
Bake it into your IaC modules
If you provision buckets through a shared Terraform or CloudFormation module, build a default lifecycle configuration into the module. Callers can override the thresholds, but a missing configuration should not be possible.
Gate it in CI/CD with policy-as-code
Use a policy engine to reject Terraform plans that create an aws_s3_bucket without an accompanying aws_s3_bucket_lifecycle_configuration. With Open Policy Agent and Conftest, a check might look like this:
package main
deny[msg] {
some i
input.resource.aws_s3_bucket[name]
not input.resource.aws_s3_bucket_lifecycle_configuration[_].bucket == name
msg := sprintf("S3 bucket '%s' has no lifecycle configuration", [name])
}
Wire that into your pull request pipeline so the build fails before the bucket ever reaches AWS.
Catch drift continuously
IaC gates only cover resources created through IaC. Buckets made by hand, by other teams, or by third-party tools slip past. Run a continuous scan across all accounts so a manually created bucket without lifecycle rules surfaces quickly. This is where Lensix runs s3_no_lifecycle_rules on a schedule and keeps the gap from sitting unnoticed.
Best practices
- Match rules to access patterns. Use S3 Storage Class Analysis or S3 Intelligent-Tiering to understand how data is read before committing to fixed transition days. Intelligent-Tiering is a strong default when access patterns are unpredictable, since it moves objects automatically with no retrieval fees.
- Always abort incomplete multipart uploads. Add this action to every bucket, with a threshold of 7 days, as a baseline. It is the cheapest, safest cleanup you can configure.
- Use prefixes and tags to scope rules. A single bucket often holds data with different lifecycles. Tag objects or organize them under prefixes so you can apply aggressive expiration to logs and gentle archiving to backups within the same bucket.
- Set expiration on versioned buckets. Versioning protects against accidental overwrites, but without
NoncurrentVersionExpirationthe old copies accumulate without limit. Pair the two. - Be deliberate about Glacier retrieval. Archiving cuts storage cost but adds retrieval latency and fees. Keep data you might need quickly in Standard-IA or Glacier Instant Retrieval rather than Deep Archive.
- Review rules periodically. Retention requirements and access patterns change. Revisit lifecycle configurations at least once a year so they keep matching reality.
Lifecycle rules are one of the highest-return, lowest-effort changes you can make in S3. A few minutes of configuration per bucket controls cost, enforces retention, and cleans up the waste that otherwise accumulates out of sight.

