Cost Management

S3 Lifecycle Policies and Storage Cost Tiers

Beginner18 min to complete9 min readAugust 14, 2026Updated August 29, 2026

Quick answer

Configure S3 Lifecycle rules to transition objects through Standard-IA and Glacier Deep Archive, expire old data automatically, and clean up abandoned multipart uploads that quietly bill you with zero console visibility.

beginner · 18 min

Before you begin

  • An AWS account
  • AWS CLI configured with credentials that can manage S3 bucket policies
  • An existing S3 bucket with some real objects in it (or willingness to create test objects)
  • Basic familiarity with S3 storage classes
AWS
S3
Cost Optimization
Storage
FinOps
Cloud Engineering

Most S3 buckets accumulate objects that get read once — an upload, an export, a build artifact — and then never touched again. Nobody configures a lifecycle policy at bucket-creation time, so that data sits in S3 Standard at full price indefinitely. A 10 TB bucket in Standard costs roughly $230/month in us-east-1; the same 10 TB in Glacier Deep Archive costs roughly $10/month. Those numbers are approximate — check current pricing for your region — but the ratio is the point: for data you're not actively serving, S3 Standard is usually the wrong tier, and the gap compounds every month nobody notices.

This tutorial writes a real Lifecycle Configuration against a bucket: transition objects to a cheaper tier as they age, expire the ones you don't need to keep at all, opt unpredictable-access data into Intelligent-Tiering instead of guessing at transition days, and clean up the abandoned multipart upload parts that don't show up in a normal object listing but still show up on the bill.

What You'll Build

  • A walk-through of the S3 storage class tiers and what each is actually for
  • A Lifecycle Configuration with Transitions rules: Standard → Standard-IA at 30 days → Glacier Deep Archive at 180 days
  • An Expiration rule that deletes objects outright after a retention window
  • An Intelligent-Tiering rule for buckets whose access patterns you can't predict
  • An AbortIncompleteMultipartUpload rule to stop paying for abandoned upload parts
  • Verification steps to confirm the rules are live and that objects actually moved

Step 1: Pick (or Create) a Test Bucket

Use a bucket you can safely experiment on — ideally one with a mix of old and recent objects so the transitions have something to act on:

bash
aws s3 ls
export BUCKET=your-test-bucket

If you don't have one handy, create a bucket and drop a few test objects in:

bash
aws s3 mb s3://your-test-bucket
# Deliberately larger than 128 KB — see the note below.
dd if=/dev/urandom of=test-file.txt bs=1024 count=200
aws s3 cp test-file.txt s3://$BUCKET/logs/2026/07/test-file.txt

Make the test object bigger than 128 KB or the rest of this tutorial will appear to do nothing. S3 Lifecycle never transitions objects smaller than 128 KB to Standard-IA, One Zone-IA, or Intelligent-Tiering's lower tiers — the per-object overhead would cost more than the storage saved. A one-line echo "test object" > test-file.txt produces a 12-byte object that will sit in Standard forever, with no error, no event, and nothing in the verification step in Step 7 to explain why. This is worth remembering beyond the tutorial: a bucket full of small objects is largely immune to IA transitions, which is a common reason a lifecycle policy fails to move the bill.

Step 2: Know the Tiers Before You Write Rules

A Lifecycle rule is only as good as your understanding of what each destination tier actually costs and constrains. In order of decreasing cost per GB and increasing retrieval friction:

  • S3 Standard — frequent access, millisecond latency, no retrieval fee. The default and the most expensive tier per GB.
  • S3 Standard-IA (and One Zone-IA) — infrequent access, same millisecond latency, but you pay a per-GB retrieval fee every time you read. One Zone-IA drops redundancy to a single AZ for a further discount — fine for easily-reproducible data, not for anything irreplaceable. Both have a 128 KB minimum for lifecycle transitions and a 30-day minimum billed duration; delete or transition an object sooner and you're still charged for the full 30 days.
  • S3 Intelligent-Tiering — automatically moves objects between access tiers based on observed access patterns, with no retrieval fee and a small per-object monitoring charge. You stop guessing transition days; AWS watches actual access and moves objects for you.
  • S3 Glacier Instant Retrieval — archive pricing with millisecond access, for data you rarely read but need immediately when you do.
  • S3 Glacier Flexible Retrieval — cheaper than Instant Retrieval; standard retrieval takes minutes to hours depending on the retrieval tier you request.
  • S3 Glacier Deep Archive — the cheapest storage AWS offers; retrieval takes hours (typically up to 12). Built for compliance archives and data you're confident you won't need on short notice.

Step 3: Write the Transition Rule

This rule moves objects from Standard to Standard-IA after 30 days, then to Glacier Deep Archive after 180 days. Save it as lifecycle.json:

json
1{
2  "Rules": [
3    {
4      "ID": "standard-to-ia-to-deep-archive",
5      "Filter": { "Prefix": "logs/" },
6      "Status": "Enabled",
7      "Transitions": [
8        {
9          "Days": 30,
10          "StorageClass": "STANDARD_IA"
11        },
12        {
13          "Days": 180,
14          "StorageClass": "DEEP_ARCHIVE"
15        }
16      ]
17    }
18  ]
19}

Apply it:

bash
aws s3api put-bucket-lifecycle-configuration \
  --bucket $BUCKET \
  --lifecycle-configuration file://lifecycle.json

Filter.Prefix scopes the rule to logs/ — the objects created in Step 1. Widen it to "" if the whole bucket should transition on this schedule, but scope it deliberately: Step 5 adds a separate rule for uploads/ on a different schedule, and a bucket-wide "" here would silently apply both to the same objects and fight over which one wins.

Step 4: Add an Expiration Rule

Transitions save money on data you're keeping; expiration stops paying for data you don't need at all — old log exports, temp exports, scratch data past its retention window. Add a second rule to the same Rules array:

json
1{
2  "ID": "expire-old-log-exports",
3  "Filter": { "Prefix": "logs/" },
4  "Status": "Enabled",
5  "Expiration": {
6    "Days": 365
7  }
8}

Re-apply with both rules present:

json
1{
2  "Rules": [
3    {
4      "ID": "standard-to-ia-to-deep-archive",
5      "Filter": { "Prefix": "logs/" },
6      "Status": "Enabled",
7      "Transitions": [
8        { "Days": 30, "StorageClass": "STANDARD_IA" },
9        { "Days": 180, "StorageClass": "DEEP_ARCHIVE" }
10      ]
11    },
12    {
13      "ID": "expire-old-log-exports",
14      "Filter": { "Prefix": "logs/" },
15      "Status": "Enabled",
16      "Expiration": { "Days": 365 }
17    }
18  ]
19}
bash
aws s3api put-bucket-lifecycle-configuration \
  --bucket $BUCKET \
  --lifecycle-configuration file://lifecycle.json

Deletion via lifecycle expiration is permanent and unrecoverable unless the bucket has versioning enabled (in which case expiration creates a delete marker instead of an outright delete). Double-check the prefix and day count before applying an expiration rule — there's no undo for the wrong prefix on a 365-day rule you don't notice for a year.

Step 5: Use Intelligent-Tiering When Access Patterns Are Unpredictable

Transition-day rules assume you know when data goes cold. If you don't — user uploads, shared datasets, anything with bursty or unpredictable reads — Intelligent-Tiering is the better default: it moves objects between frequent, infrequent, and (optionally) archive access tiers automatically based on 30 days of observed access, with no retrieval fee. You pay a small monitoring fee per object (roughly $0.0025 per 1,000 objects/month, check current pricing), which is why it's not the default for every bucket — for a huge count of small objects that monitoring fee can outweigh the savings.

Opt objects in as a bucket-wide lifecycle rule:

json
1{
2  "ID": "auto-tier-uploads",
3  "Filter": { "Prefix": "uploads/" },
4  "Status": "Enabled",
5  "Transitions": [
6    {
7      "Days": 0,
8      "StorageClass": "INTELLIGENT_TIERING"
9    }
10  ]
11}

Or opt in per-object at upload time, skipping the lifecycle rule entirely for that object:

bash
aws s3 cp report.csv s3://$BUCKET/uploads/report.csv \
  --storage-class INTELLIGENT_TIERING

Rule of thumb: use manual Transitions (Step 3) when you can state a confident access cutoff — "nobody reads build artifacts after 30 days." Use Intelligent-Tiering when you can't state that cutoff and the per-object monitoring fee is smaller than the cost of guessing wrong in either direction.

Step 6: Clean Up Abandoned Multipart Uploads

Multipart uploads that never complete — a crashed upload client, an interrupted CI job — leave parts sitting in the bucket. They don't appear in a normal ListObjects call or the console's default object browser, so they cost money with effectively zero visibility unless you go looking. Add this rule; it's cheap insurance and belongs in every bucket's lifecycle configuration:

json
1{
2  "ID": "abort-incomplete-multipart-uploads",
3  "Filter": { "Prefix": "" },
4  "Status": "Enabled",
5  "AbortIncompleteMultipartUpload": {
6    "DaysAfterInitiation": 7
7  }
8}

Fold it into the same configuration as the other rules and re-apply with put-bucket-lifecycle-configuration as before.

Step 7: Verify the Rules Are Active

Confirm the configuration landed:

bash
aws s3api get-bucket-lifecycle-configuration --bucket $BUCKET

You should see all four rules — transitions, expiration, Intelligent-Tiering opt-in, and the multipart abort rule — each with "Status": "Enabled".

Lifecycle transitions run on a schedule S3 manages, not instantly, so you won't see an object move tiers the moment a rule is applied. To spot-check a specific object's current storage class once enough time has passed:

bash
aws s3api list-objects-v2 --bucket $BUCKET --prefix logs/ \
  --query 'Contents[].{Key:Key,StorageClass:StorageClass}'

Objects still in Standard show no StorageClass field (Standard is the implicit default); transitioned objects report STANDARD_IA, DEEP_ARCHIVE, or INTELLIGENT_TIERING explicitly. For bucket-wide storage-class breakdowns and cost trends over time rather than one-off spot checks, use S3 Storage Lens in the console — it's built for exactly this and doesn't require scripting a full-bucket scan.

One caution worth stating plainly: Glacier retrieval is not instant, except for Glacier Instant Retrieval. Standard Glacier Flexible Retrieval takes minutes to hours, and Glacier Deep Archive takes up to about 12 hours, plus a per-GB retrieval fee on top. This tutorial is about data you're confident you won't need on short notice — it is not a general-purpose cost lever to apply to everything in a bucket without checking access patterns first.

Where to Go Next

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.