BlogCloud & Infrastructure
Cloud & Infrastructure

Cloud Storage Cost Guide: S3, EBS, EFS, and FSx β€” Choosing the Right Tier

Storage costs are invisible until they are not. S3 Intelligent-Tiering, EBS gp3 migration, and choosing EFS Standard vs IA can easily cut storage bills by 40-70%. This guide maps every AWS storage service to its cost profile, access pattern, and optimization strategies.

E

Emily Watson

Technical Writer and Developer Advocate who simplifies complex technology for everyday readers.

April 9, 2026
21 min read

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.

E

Emily Watson

Technical Writer and Developer Advocate who simplifies complex technology for everyday readers.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.