The most common thing teams say when they see a large S3 bill is some version of "we only store a few hundred gigabytes, this can't be right." They're looking at the storage line and ignoring everything below it. At $0.023/GB for standard storage, S3 storage is genuinely cheap. The egress line at $0.09/GB is nearly four times more expensive per unit, and it can appear out of nowhere when someone adds a public download endpoint or an EC2 service starts streaming data out to the internet.

Over 30 years in IT, the S3 bill surprises follow a consistent pattern. Four line items account for almost every unexpected charge I've seen: egress, request costs, Glacier retrievals, and replication. Here's how to find each one and what to do about it.

The Four Lines That Grow Silently

1. Data Transfer Out (Egress): $0.09/GB

This is the one. S3 to the internet costs $0.09 per GB for the first 10 TB per month. That sounds manageable until you realize a service streaming 100 GB/day to end users is running up a $270/month egress bill on top of whatever you expected to pay. S3 to EC2 in the same region is free, but S3 to EC2 in a different region is not. Cross-region transfers run $0.02/GB, and they don't show up labeled as "cross-region" in an obvious way.

Watch for this: If you have a public S3 bucket serving files directly to users, you're paying $0.09/GB for every byte delivered. The fix is CloudFront in front of S3. CloudFront-to-internet pricing is lower than S3-direct, and caching means most requests never touch S3 at all.

To find egress charges in Cost Explorer: set the service filter to "S3," then use the "Usage Type" filter and look for DataTransfer-Out-Bytes. Group by usage type and you'll see exactly which transfer category is driving the bill.

2. Request Costs: PUT $0.005/1000, GET $0.0004/1000

Per-request pricing feels negligible at human scale. At machine scale, it isn't. A misconfigured service that retries a failed read every 100ms against an S3 object is making 864,000 GET requests per day. That's about $0.35/day per misconfigured instance, which adds up to $127/year per instance before you've noticed anything. With ten instances doing it, you're looking at $1,270/year from a bug nobody filed a ticket for.

PUT requests are 12.5x more expensive per unit than GET requests, at $0.005 per thousand versus $0.0004 per thousand. An application that writes a new object for every event or log line will chew through PUT costs faster than you'd expect. The fix is usually batching: write fewer, larger objects instead of many small ones.

Cost Explorer tip: Filter by service "S3" and group by "Usage Type." Look for Requests-Tier1 (PUT, COPY, POST, LIST) and Requests-Tier2 (GET, HEAD) line items. If either is unusually high relative to your storage volume, something is making too many API calls.

3. Glacier Retrieval: Expedited $0.03/GB, Standard $0.01/GB, Bulk $0.0025/GB

The lifecycle policy mistake is one of the most painful S3 billing surprises I've seen. Someone sets a lifecycle rule to transition objects to Glacier after 30 days to save money. That's sensible. Then six months later, an engineer needs to restore a dataset for debugging and clicks "expedited retrieval" without looking at the pricing. At $0.03/GB, retrieving 1 TB costs $30.72 in retrieval fees alone, on top of the standard transfer costs out.

Standard retrieval at $0.01/GB is more reasonable for planned restores (3-5 hour wait). Bulk at $0.0025/GB is the right choice for large restores where you can wait 5-12 hours. The problem is almost never the retrieval tier choice. The problem is that teams set lifecycle policies to archive data they actually need to access regularly, treating Glacier like cheap standard storage instead of a true archive tier.

Retrieval Tier Cost Restore Time 1 TB Retrieval Cost
Expedited $0.03/GB 1-5 minutes $30.72
Standard $0.01/GB 3-5 hours $10.24
Bulk $0.0025/GB 5-12 hours $2.56

Audit your lifecycle policies quarterly. For anything that gets accessed more than a few times per year, Glacier is the wrong storage class. S3 Infrequent Access at $0.0125/GB/month costs more per month than Glacier, but has no retrieval fees and returns data immediately.

4. Cross-Region Replication: You Pay Twice

S3 replication for disaster recovery or compliance is a legitimate use case. What teams routinely underestimate is the total cost. You pay for storage in the source region and again in the destination region. You pay the replication data transfer fee of $0.02/GB (same-region replication is free; cross-region is not). And you pay the PUT request cost for every object written to the destination bucket.

For a bucket with 10 TB of data and 100 GB of daily churn, cross-region replication adds roughly $2.00/day in transfer fees plus the doubled storage cost. That's not catastrophic, but it's real money that rarely appears in initial architecture cost estimates.

The Fix for Egress: CloudFront

If you're serving S3 objects to end users, CloudFront in front of S3 is almost always the right call on cost alone. CloudFront's data transfer to internet pricing starts at $0.0085/GB (10x cheaper than S3's $0.09/GB in some regions) and cached content means the majority of requests never reach S3 at all, cutting both egress and request costs simultaneously.

The S3 origin access control setup takes about 20 minutes. Set the bucket to block all public access, create a CloudFront distribution, configure origin access control, and update the bucket policy. After that, all content delivery routes through CloudFront and your S3 egress line should drop close to zero.

The One You Forget: S3 Inventory

S3 Inventory is easy to set up, useful for auditing large buckets, and easy to forget you turned on. It runs daily or weekly and generates a CSV or Parquet file listing every object in your bucket. The cost is $0.0025 per million objects listed. For a bucket with 500 million objects running daily inventory, that's $1.25/day, or $456/year, for a report most teams have stopped reading.

Check your S3 bucket configurations for active inventory reports. If nobody is consuming the output, turn them off. If you need inventory for a one-time audit, run it once and disable it.

Quick Cost Explorer checklist for S3: Filter by service "S3" and group by Usage Type. Look for: DataTransfer-Out-Bytes (egress), Requests-Tier1 / Requests-Tier2 (API calls), Glacier-Retrieval variants, and TimedStorage-RRS-ByteHrs for unexpected storage class charges. Sort by cost descending and anything above $5/month deserves a look.

What to Model Before You Build

The teams that avoid S3 bill surprises are the ones that model egress before the architecture is finalized, not after. For any bucket that serves data to external users or other regions, run the numbers at three scales: current, 10x current, and 100x current. If the egress cost at 10x breaks your budget, design CloudFront caching in from the start.

Request costs are worth modeling separately if your application writes or reads from S3 at high frequency. More than a few hundred thousand requests per day is the point where per-request pricing becomes a line item worth watching, not ignoring.

For everything else, S3 pricing is genuinely straightforward if you read the full pricing page instead of stopping at the storage line. The surprises are almost always charges that were always documented, just not in the part of the page the architect read before signing off on the design.