Auto-Deleting Old S3 Objects: How to Set Up a Lifecycle Rule for 30-Day Expiration

Auto-deleting old S3 objects with a Lifecycle Rule is the standard way to enforce 30-day retention and eliminate storage costs from accumulating stale data.

TL;DR: S3 Lifecycle Expiration in 60 Seconds

StepActionWhere
1Open bucket → Management → Lifecycle rulesS3 Console
2Create rule with Expiration: 30 daysConsole or CLI
3Optionally scope to a prefix or tagRule filter
4Verify rule is Enabled and confirm no conflicting rulesConsole / CLI

Why Auto-Deleting Old S3 Objects Matters for Cost Control

S3 charges for every GB stored, every day. Without an expiration policy, buckets silently accumulate objects — log files, temporary uploads, pipeline artifacts — long after they serve any purpose. A single misconfigured ETL job writing 10 GB/day produces 300 GB of orphaned data in a month. A Lifecycle Rule (S3's built-in object expiration mechanism) removes objects automatically on a schedule you define, with no Lambda, no cron job, and no per-deletion API cost.

Think of a Lifecycle Rule like a lease agreement on a storage unit: when the lease expires, the contents are cleared automatically — you don't need to show up in person.

How S3 Lifecycle Expiration Works

S3 evaluates Lifecycle rules daily. When an object's age (measured from its Last-Modified date) meets or exceeds the configured expiration threshold, S3 schedules it for deletion. The deletion itself may occur up to 24 hours after the threshold is crossed — this is expected behavior, not a bug.

flowchart TD A["Object Uploaded Last-Modified recorded"] --> B["Daily Lifecycle Evaluation"] B --> C{"Age ≥ 30 days?"} C -- No --> D["Object retained Re-evaluated next day"] C -- Yes --> E["Object marked for expiration"] E --> F{"Versioning enabled?"} F -- No --> G["Object permanently deleted"] F -- Yes --> H["Delete marker inserted Previous version becomes non-current"] H --> I["NoncurrentVersionExpiration fires after configured days"] I --> J["Non-current version permanently deleted"] G --> K["Storage billing stops"] J --> K
  1. Object Created: S3 records the Last-Modified timestamp at upload time.
  2. Daily Evaluation: S3's internal Lifecycle engine scans objects against all enabled rules for the bucket.
  3. Age Check: If current_date − Last-Modified ≥ 30 days, the object is marked for expiration.
  4. Deletion Executed: S3 permanently deletes the object (or, for versioned buckets, inserts a delete marker — see the versioning note below).
  5. Storage Billing Stops: The object no longer appears in storage metrics or billing after deletion.

Versioning Changes the Deletion Model — Read This First

This is the most common production gotcha with S3 Lifecycle rules.

Symptom: You configure a 30-day expiration rule, but your bucket storage keeps growing and objects remain visible in the console.
Misdiagnosis: Engineers assume the rule isn't working or wasn't saved correctly.
Actual cause: Bucket versioning is enabled. On a versioned bucket, an expiration action on current versions inserts a delete marker rather than permanently removing the object. The non-current versions and the delete markers themselves continue to consume storage until separate rule actions target them explicitly.

For versioned buckets, you need three coordinated actions in your Lifecycle rule:

  • Expiration → expires current object versions (inserts delete marker)
  • NoncurrentVersionExpiration → permanently deletes non-current versions after N days
  • ExpiredObjectDeleteMarker → cleans up orphaned delete markers
flowchart LR subgraph Unversioned["Unversioned Bucket"] U1["Current Object"] -->|"Expiration: 30 days"| U2["Permanently Deleted"] end subgraph Versioned["Versioned Bucket"] V1["Current Version"] -->|"Expiration action"| V2["Delete Marker inserted"] V1 -->|"becomes"| V3["Non-current Version"] V3 -->|"NoncurrentVersionExpiration: 30 days"| V4["Permanently Deleted"] V2 -->|"ExpiredObjectDeleteMarker: true"| V5["Delete Marker removed"] end
  1. Unversioned path (left): Expiration directly and permanently deletes the object.
  2. Versioned path (right): Expiration on the current version inserts a delete marker. The previous version becomes non-current and persists until NoncurrentVersionExpiration fires.
  3. Delete marker cleanup: Once all non-current versions are gone, ExpiredObjectDeleteMarker: true removes the orphaned marker, fully freeing storage.

Setting Up the Lifecycle Rule: Two Approaches

Choose based on your workflow. The AWS CLI approach is repeatable and version-controllable; the console is faster for one-off configurations.

Approach A (Recommended): AWS CLI — Repeatable and Scriptable

The CLI accepts a JSON configuration file, making the rule auditable and deployable via CI/CD pipelines. The example below covers both unversioned and versioned bucket scenarios in a single rule.

First, create the Lifecycle configuration JSON file:

🔽 lifecycle-config.json — Click to expand
{
  "Rules": [
    {
      "ID": "delete-objects-older-than-30-days",
      "Status": "Enabled",
      "Filter": {
        "Prefix": ""
      },
      "Expiration": {
        "Days": 30
      },
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30
      },
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    }
  ]
}

Apply the configuration to your bucket:

aws s3api put-bucket-lifecycle-configuration \
  --bucket YOUR-BUCKET-NAME \
  --lifecycle-configuration file://lifecycle-config.json

Verify the rule was applied correctly — this step catches silent save failures that the console sometimes masks:

aws s3api get-bucket-lifecycle-configuration \
  --bucket YOUR-BUCKET-NAME

Expected output includes your rule with "Status": "Enabled". If the command returns a NoSuchLifecycleConfiguration error, the rule was not saved.

To add delete marker cleanup for versioned buckets, update the rule to include the Expiration.ExpiredObjectDeleteMarker flag. Note that ExpiredObjectDeleteMarker and Days under Expiration are mutually exclusive — use separate rules if you need both behaviors:

🔽 lifecycle-config-versioned.json — Click to expand
{
  "Rules": [
    {
      "ID": "expire-current-versions-30-days",
      "Status": "Enabled",
      "Filter": {
        "Prefix": ""
      },
      "Expiration": {
        "Days": 30
      },
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30
      },
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    },
    {
      "ID": "cleanup-expired-delete-markers",
      "Status": "Enabled",
      "Filter": {
        "Prefix": ""
      },
      "Expiration": {
        "ExpiredObjectDeleteMarker": true
      }
    }
  ]
}

Approach B: AWS Console — Fast for One-Off Rules

  1. Navigate to S3 → your bucket → Management → Lifecycle rules → Create lifecycle rule.
  2. Enter a rule name (e.g., delete-objects-older-than-30-days).
  3. Under Filter type, leave the prefix blank to apply to all objects, or enter a prefix (e.g., logs/) to scope the rule.
  4. Under Lifecycle rule actions, check Expire current versions of objects.
  5. Set Days after object creation to 30.
  6. If versioning is enabled, also check Permanently delete noncurrent versions of objects and set the days value.
  7. Review and save. Confirm the rule shows Enabled in the rules list.

Scoping Rules to a Prefix or Tag

Applying a rule to the entire bucket is appropriate for single-purpose buckets. For shared buckets, scope the rule using a prefix or object tag to avoid unintended deletions.

Prefix-scoped example — targets only objects under tmp/uploads/:

{
  "Rules": [
    {
      "ID": "expire-tmp-uploads-30-days",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "tmp/uploads/"
      },
      "Expiration": {
        "Days": 30
      }
    }
  ]
}

Tag-scoped example — targets objects tagged retention=short:

{
  "Rules": [
    {
      "ID": "expire-short-retention-30-days",
      "Status": "Enabled",
      "Filter": {
        "Tag": {
          "Key": "retention",
          "Value": "short"
        }
      },
      "Expiration": {
        "Days": 30
      }
    }
  ]
}

Tag-based filtering requires that objects are tagged at upload time. If tags are missing, the rule silently skips those objects — there is no error or warning.

IAM Permissions Required

The IAM principal applying the Lifecycle rule needs the following permissions. Note that s3:GetLifecycleConfiguration and s3:PutLifecycleConfiguration operate at the bucket level, not the object level.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetLifecycleConfiguration",
        "s3:PutLifecycleConfiguration"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME"
    }
  ]
}

S3 itself performs the actual object deletions using its internal service principal — your IAM policy does not need s3:DeleteObject for the Lifecycle engine to function.

Diagnosing a Lifecycle Rule That Appears Not to Fire

In practice, teams often assume a rule is broken when objects persist past day 30. Before re-creating the rule, work through these checks in order — each one catches what the previous step cannot.

  1. Confirm the rule status is Enabled — a rule saved as Disabled never evaluates, and the console does not warn you at creation time.
    aws s3api get-bucket-lifecycle-configuration \
      --bucket YOUR-BUCKET-NAME \
      --query 'Rules[*].{ID:ID,Status:Status}'
  2. Check whether versioning is enabled on the bucket — if it is, current-version expiration inserts delete markers rather than permanently deleting objects, which is why storage appears unchanged even after the rule fires.
    aws s3api get-bucket-versioning \
      --bucket YOUR-BUCKET-NAME
    If the output shows "Status": "Enabled", add NoncurrentVersionExpiration and the delete marker cleanup rule as shown in the versioned config above.
  3. Verify the object's Last-Modified date — Lifecycle age is calculated from Last-Modified, not from any application-level timestamp. An object copied or re-uploaded resets this clock.
    aws s3api head-object \
      --bucket YOUR-BUCKET-NAME \
      --key path/to/your/object.log
    Inspect the LastModified field in the response.
  4. Check for an S3 Object Lock on the bucket — Object Lock (a WORM compliance mechanism) can prevent Lifecycle deletions from completing. If Object Lock is active, Lifecycle expiration may be blocked for locked objects.
    aws s3api get-object-lock-configuration \
      --bucket YOUR-BUCKET-NAME
    If ObjectLockEnabled is Enabled, review the retention mode and period configured on individual objects.

Lifecycle evaluation happens once per day — a rule applied today will not retroactively delete objects that crossed the 30-day threshold yesterday until the next evaluation cycle runs.

Cost Impact and the AbortIncompleteMultipartUpload Bonus

Object expiration eliminates storage charges for deleted objects. One frequently overlooked cost source is incomplete multipart uploads: parts uploaded but never completed or aborted accumulate storage charges indefinitely and are invisible in standard S3 object listings. The AbortIncompleteMultipartUpload action in the example configurations above cleans these up automatically after 7 days.

Pricing and storage class rates vary — always verify current costs in the AWS S3 Pricing page.

flowchart TD A["S3 Bucket Objects"] --> B["Standard Objects"] A --> C["Incomplete Multipart Uploads"] A --> D["Non-current Versions (versioned buckets only)"] B -->|"Expiration: Day 30"| E["Permanently Deleted"] C -->|"AbortIncompleteMultipartUpload: Day 7"| F["Parts Aborted & Removed"] D -->|"NoncurrentVersionExpiration: Day 30"| G["Permanently Deleted"] E --> H["Storage Cost Eliminated"] F --> H G --> H
  1. Standard objects: Deleted at day 30 by the Expiration action.
  2. Incomplete multipart uploads: Aborted at day 7 by AbortIncompleteMultipartUpload, preventing silent storage accumulation.
  3. Non-current versions (versioned buckets): Permanently deleted at day 30 after becoming non-current via NoncurrentVersionExpiration.

Wrap-Up: Auto-Deleting Old S3 Objects Without Surprises

A single S3 Lifecycle Rule handles 30-day object expiration with no ongoing operational overhead. The two decisions that determine whether it works correctly in production are: (1) whether versioning is enabled, and (2) whether the rule filter matches the objects you intend to target. Verify both before declaring the rule complete.

For further reading, see the AWS S3 Object Lifecycle Management documentation and the Lifecycle configuration examples.

Glossary

TermDefinition
Lifecycle RuleAn S3 bucket-level policy that automates transitions and expirations of objects based on age or other criteria.
Expiration ActionA Lifecycle rule action that permanently deletes an object (or inserts a delete marker on versioned buckets) after a specified number of days.
Delete MarkerA placeholder inserted by S3 when a versioned object is "deleted"; the object's previous versions remain until explicitly removed.
NoncurrentVersionExpirationA Lifecycle action that permanently deletes non-current (previous) versions of objects in a versioned bucket after a specified number of days.
AbortIncompleteMultipartUploadA Lifecycle action that cancels and removes multipart upload parts that were never completed, preventing silent storage cost accumulation.

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?