The Storage Cost Problem
Storage costs compound silently. Engineers provision volumes, load data into S3, and forget about it. Over months, EBS snapshots accumulate, S3 buckets fill with never-accessed objects, and EFS mounts hold gigabytes of stale build artifacts. Storage spend can easily reach 15-25% of total AWS bill for data-heavy workloads.
The good news: storage optimization has some of the best ROI of any cloud cost work. S3 Intelligent-Tiering can automatically move objects to 68-99% cheaper storage classes. EBS gp3 delivers the same or better performance as gp2 at 20% lower cost. These are mostly configuration changes, not architectural rewrites.
S3 Storage Classes: Full Cost Comparison
S3 Storage Class Pricing (us-east-1, per GB/month):
Storage Class | Storage | Retrieval | Min Duration | Use Case
------------------------|-----------|------------|--------------|------------------
S3 Standard | $0.023 | Free | None | Frequently accessed
S3 Intelligent-Tiering | $0.023 | Free | None | Unknown patterns
- Frequent tier | $0.023 | Free | - | Auto-managed
- Infrequent tier | $0.0125 | Free | 30 days | Auto after 30d
- Archive Instant | $0.004 | Free | 90 days | Auto after 90d
S3 Standard-IA | $0.0125 | $0.01/GB | 30 days | Monthly access
S3 One Zone-IA | $0.01 | $0.01/GB | 30 days | Reproducible data
S3 Glacier Instant | $0.004 | $0.03/GB | 90 days | Quarterly access
S3 Glacier Flexible | $0.0036 | $0.01/GB | 90 days | Archival (hr delay)
S3 Glacier Deep Archive | $0.00099 | $0.02/GB | 180 days | Compliance archives
Key insight: Intelligent-Tiering monitoring fee is $0.0025/1K objects
For objects > 128KB, Intelligent-Tiering almost always pays off.
For objects < 128KB, the per-object fee makes it expensive β use lifecycle rules instead.
S3 Lifecycle Rules: Automate Tier Transitions
# Terraform: S3 lifecycle policy for logs bucket
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "log-lifecycle"
status = "Enabled"
filter {
prefix = "logs/" # Apply only to logs/ prefix
}
# Transition to IA after 30 days ($0.023 β $0.0125)
transition {
days = 30
storage_class = "STANDARD_IA"
}
# Transition to Glacier after 90 days ($0.0125 β $0.004)
transition {
days = 90
storage_class = "GLACIER"
}
# Transition to Deep Archive after 365 days ($0.004 β $0.00099)
transition {
days = 365
storage_class = "DEEP_ARCHIVE"
}
# Delete after 7 years (compliance)
expiration {
days = 2555 # 7 years
}
# Delete failed multipart uploads (common cost leak)
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
# For analytics/ML datasets: Intelligent-Tiering
resource "aws_s3_bucket_intelligent_tiering_configuration" "datasets" {
bucket = aws_s3_bucket.ml_datasets.id
name = "auto-tiering"
status = "Enabled"
# Auto-move to Archive Instant after 90 days of no access
tiering {
access_tier = "ARCHIVE_ACCESS"
days = 90
}
# Auto-move to Deep Archive after 180 days
tiering {
access_tier = "DEEP_ARCHIVE_ACCESS"
days = 180
}
}
EBS: Migrate gp2 to gp3
gp2 vs gp3 cost comparison:
gp2: $0.10/GB/month, IOPS burst to 3,000 (capped)
gp3: $0.08/GB/month, 3,000 IOPS baseline included
For same IOPS performance: gp3 is 20% cheaper
For high-IOPS needs: gp2 costs $0.065/IOPS above 3K, gp3 $0.005/IOPS
Example: 1TB database volume needing 10,000 IOPS
gp2: $0.10 Γ 1024GB = $102.40/month (cannot get 10K IOPS)
Must use io1/io2: $125 + $0.065 Γ 10,000 = $775/month
gp3: $0.08 Γ 1024GB + $0.005 Γ 7,000 IOPS = $81.92 + $35 = $116.92/month
Saving vs io1: $658/month (85%!)
Saving vs gp2 same IOPS: impossible with gp2
# Find all gp2 volumes and calculate migration savings
aws ec2 describe-volumes --filters "Name=volume-type,Values=gp2" --query 'Volumes[*].{ID:VolumeId,Size:Size,IOPS:Iops,State:State,AZ:AvailabilityZone}' --output table
# Migrate a single volume from gp2 to gp3 (online, no downtime)
aws ec2 modify-volume --volume-id vol-0123456789abcdef0 --volume-type gp3 --iops 3000 --throughput 125
# Bulk migrate all gp2 volumes to gp3
aws ec2 describe-volumes --filters "Name=volume-type,Values=gp2" --query 'Volumes[*].VolumeId' --output text | tr ' ' '
' | while read vol_id; do
echo "Migrating $vol_id to gp3..."
aws ec2 modify-volume --volume-id "$vol_id" --volume-type gp3 --iops 3000 --throughput 125
done
EBS Snapshot Cleanup
import boto3
from datetime import datetime, timezone, timedelta
ec2 = boto3.client('ec2', region_name='us-east-1')
def find_orphaned_snapshots():
"""Find snapshots not attached to any AMI and older than 90 days."""
# Get all snapshots owned by this account
snapshots = ec2.describe_snapshots(OwnerIds=['self'])['Snapshots']
# Get all AMI snapshot IDs
images = ec2.describe_images(Owners=['self'])['Images']
ami_snapshot_ids = set()
for image in images:
for bdm in image.get('BlockDeviceMappings', []):
if 'Ebs' in bdm:
ami_snapshot_ids.add(bdm['Ebs']['SnapshotId'])
# Find orphaned old snapshots
cutoff = datetime.now(timezone.utc) - timedelta(days=90)
orphaned = []
total_size_gb = 0
for snap in snapshots:
if (snap['SnapshotId'] not in ami_snapshot_ids and
snap['StartTime'] < cutoff):
orphaned.append(snap)
total_size_gb += snap['VolumeSize']
monthly_cost = total_size_gb * 0.05 # EBS snapshot: $0.05/GB/month
print(f"Orphaned snapshots: {len(orphaned)}")
print(f"Total size: {total_size_gb:,} GB")
print(f"Monthly waste: monthly_cost:,.2f")
return orphaned
orphaned = find_orphaned_snapshots()
# Review list, then delete:
# for snap in orphaned:
# ec2.delete_snapshot(SnapshotId=snap['SnapshotId'])
EFS: Standard vs Infrequent Access
EFS Storage Pricing (us-east-1):
EFS Standard: $0.30/GB/month (always hot)
EFS Standard-IA: $0.025/GB/month (retrieval: $0.01/GB)
Comparison: IA is 88% cheaper for storage
Break-even: if <42% of data is accessed monthly, IA is cheaper
EFS Intelligent Tiering (Lifecycle Management):
Automatically moves files to IA after 7, 14, 30, 60, or 90 days of no access
Example: 5TB EFS mount, 20% frequently accessed, 80% stale
All Standard: 5,120GB Γ $0.30 = $1,536/month
With IA tiering: 1,024GB Γ $0.30 + 4,096GB Γ $0.025 = $307 + $102 = $409/month
Saving: $1,127/month (73%)
# Terraform: EFS with Intelligent Tiering
resource "aws_efs_file_system" "shared" {
creation_token = "shared-storage"
performance_mode = "generalPurpose"
throughput_mode = "elastic" # Pay per GB transferred, not provisioned
lifecycle_policy {
transition_to_ia = "AFTER_30_DAYS" # Move to IA after 30 days
}
lifecycle_policy {
transition_to_primary_storage_class = "AFTER_1_ACCESS" # Return on access
}
tags = {
Name = "shared-storage"
}
}
resource "aws_efs_mount_target" "az_a" {
file_system_id = aws_efs_file_system.shared.id
subnet_id = aws_subnet.private_a.id
security_groups = [aws_security_group.efs.id]
}
Storage Type Decision Framework
Decision Tree: Which storage to use?
Is data block-level (OS volumes, databases)?
YES β Use EBS
High IOPS (>3,000)? β gp3 (up to 16,000 IOPS)
Very high IOPS (>64,000)? β io2 Block Express
Low cost, no IOPS requirements? β st1 (HDD, $0.045/GB)
Archival EBS? β sc1 (cold HDD, $0.018/GB)
Is data shared across multiple EC2/ECS/EKS instances?
YES β Use EFS (or FSx for Windows/high-perf)
Linux workloads β EFS
Windows workloads β FSx for Windows
High-performance HPC β FSx for Lustre
Is data object-level (files, backups, ML datasets, media)?
YES β Use S3
Frequently accessed daily β S3 Standard
Unknown access pattern β S3 Intelligent-Tiering
Monthly/quarterly access β S3 Standard-IA
Yearly access β S3 Glacier
Compliance archive β S3 Glacier Deep Archive
Reproducible data (thumbnails) β S3 One Zone-IA
Monthly cost per 1TB:
EBS gp3: $81.92
EFS Standard: $307
EFS IA: $25
S3 Standard: $23
S3 IA: $12.80
S3 Glacier: $4.10
S3 Deep Archive: $1.01
Conclusion
Storage optimization follows a simple pattern: analyze what you have, classify access patterns, and apply the appropriate tier. The migration from gp2 to gp3 alone is a no-downtime 20% savings. S3 Intelligent-Tiering and EFS lifecycle policies can reduce storage costs by 70%+ automatically, without manual intervention.
Audit your storage monthly. Delete orphaned EBS snapshots, enable S3 Intelligent-Tiering on large buckets, and move EFS mounts to lifecycle-managed tiers. Storage costs compound over time, but so do the savings from getting this right.
Emily Watson
Technical Writer and Developer Advocate who simplifies complex technology for everyday readers.