On most AWS invoices, compute and storage are the numbers people stare at because they map cleanly to something you asked for: instances you launched, gigabytes you stored. Data transfer is the category nobody reads, because it doesn't map to a resource you can point at — it's a tax on packets in motion, accrued by traffic patterns you never consciously chose. It is also, on a surprising share of bills, the biggest line. Here's how each of its pieces actually works, and where a "normal" architecture leaks money by design.
Internet egress and the tiered curve
Data leaving AWS for the public internet is billed per gigabyte, on a declining tiered curve. In us-east-1 the first tier is roughly $0.09/GB, covering the first 10 TB per month, and the rate steps down as you cross 10 TB, 50 TB, and 150 TB in a month. Data in from the internet is free. The rates vary by region — some are noticeably higher — so a workload that looks cheap in Virginia can cost more in São Paulo or Cape Town for identical traffic.
The mental trap is that $0.09 sounds trivial. It is trivial per gigabyte and ruinous per terabyte: 50 TB of egress lands somewhere around $4,000–$4,500 a month depending on how the tiers blend. Nobody decides to spend that. It accumulates one API response and one video segment at a time.
Cross-AZ traffic, billed in both directions
Inside a region, traffic that crosses an Availability Zone boundary is charged about $0.01/GB — and here is the part that catches people — it is billed on both ends. A gigabyte sent from an instance in AZ-a to an instance in AZ-b incurs $0.01 leaving and $0.01 arriving, so the round number to keep in your head is $0.02/GB for a cross-AZ exchange.
This is the charge that hides inside "high availability." You spread a service across three AZs for resilience, and now every chatty internal call — app tier to database replica, service mesh sidecar to sidecar, a Kafka consumer pulling from a broker in another zone — may be crossing a zone boundary and paying the toll each way. The traffic is real work, so it's easy to miss that a meaningful fraction of it is paying $0.02/GB purely because the two endpoints happened to land in different AZs. Zone-aware routing, keeping tightly-coupled chatter within an AZ, and being deliberate about which replica a client reads from are the levers here.
The NAT Gateway double charge
The NAT Gateway is the most reliably surprising line on the bill because it charges you twice for the same byte. There is an hourly charge for the gateway existing (roughly $0.045/hour, about $32/month before any traffic), and a per-GB processing charge (again roughly $0.045/GB) on everything that flows through it. Both, stacked.
Now picture a private subnet full of instances that need the internet only for outbound things — pulling container images, hitting a third-party API, downloading OS packages, shipping logs. Every one of those bytes is processed by the NAT Gateway at $0.045/GB, and if the destination is the public internet it then also pays internet egress. A chatty fleet that pulls a few hundred gigabytes of images and dependencies a day can run the NAT processing charge into hundreds of dollars a month on its own, entirely separate from the egress it feeds into.
The infuriating special case: traffic from a private subnet to S3 or DynamoDB, routed out through the NAT Gateway to reach the public endpoint, pays the $0.045/GB NAT processing charge — for traffic that never needed to leave AWS at all. That's the setup for the next piece.
Gateway endpoints (free) versus Interface endpoints (not)
VPC endpoints let private resources reach AWS services without a NAT Gateway or internet path. There are two kinds and the pricing difference between them is stark.
Gateway Endpoints exist for exactly two services — S3 and DynamoDB — and they are free. They work by injecting routes into your subnet route table so traffic to those services goes directly over the AWS network, never touching the NAT Gateway. If your private instances talk to S3 or DynamoDB and you don't have a Gateway Endpoint, you are paying NAT processing on that traffic for no reason. Adding the endpoint is close to a pure win: it removes the NAT charge and the internet-path exposure. This one change alone has rescued more surprise bills than any other item on this list.
Interface Endpoints (AWS PrivateLink) cover the long tail of other services — SQS, ECR, Secrets Manager, CloudWatch, and so on. They provision an elastic network interface in your subnets and are billed like a NAT Gateway in spirit: an hourly charge per endpoint per AZ (roughly $0.01/hour each) plus a per-GB processing charge. They're often still worth it — the per-GB rate can undercut NAT processing plus egress, and you gain a private path — but they are not free, and standing up a dozen of them across three AZs is its own recurring line. Do the arithmetic per service against what the NAT path would cost.
CloudFront egress is cheaper than raw egress
One more lever that's easy to overlook: serving bytes to users through CloudFront is cheaper per gigabyte than serving them straight from EC2 or S3, and the origin fetch from S3 to CloudFront is not billed as egress at all. So a design where users pull large objects directly from an S3 bucket at ~$0.09/GB is paying more than the same objects served via CloudFront, where the edge rate is lower and the S3-to-edge hop is free. If you're serving cacheable content at volume without a CDN in front, you are choosing the expensive door.
A worked example
Consider a media-ish API on ECS across three AZs. The monthly transfer picture:
- Internet egress: 40 TB of responses and downloads to users, blended to about $0.087/GB after tiering ≈ $3,500.
- Cross-AZ: the app tier chats heavily with a database and a cache spread across zones — say 30 TB crossing AZ boundaries, at $0.02/GB round-trip ≈ $600.
- NAT Gateway: two gateways for HA at ~$32/month each ≈ $64 hourly, plus 8 TB/month of outbound pulls and, crucially, S3 traffic misrouted through NAT at $0.045/GB ≈ $360 processing. Call it ~$425.
- Total transfer: roughly $4,500/month — very possibly larger than the compute running the service.
Now the cuts, in order of effort-to-payoff:
- Add an S3 Gateway Endpoint. The S3 traffic stops traversing NAT. NAT processing drops immediately; this is free and takes minutes.
- Put CloudFront in front of the cacheable egress. A high cache-hit ratio moves most of that 40 TB to the cheaper edge rate and eliminates the S3-origin egress on hits. The $3,500 is where the real money is, and this is the biggest single reduction.
- Make the internal chatter zone-aware. Route reads to a same-AZ replica and keep tightly-coupled services co-located, and a large slice of the $600 cross-AZ line evaporates.
- Consolidate NAT usage. For the remaining internet-bound pulls, cache container images and packages, and reach for Interface Endpoints where the per-GB math beats NAT.
The habit worth building
Data transfer resists optimization because it's invisible until you go looking. Cost Explorer will happily break spend down by the usage type codes — DataTransfer-Out-Bytes, DataTransfer-Regional-Bytes, NatGateway-Bytes, VpcEndpoint-Bytes — and reading that breakdown once a month is the single highest-leverage cost habit I know of. The fixes are almost always architectural rather than a matter of buying a cheaper tier: keep bytes inside AWS when they don't need to leave, keep them inside an AZ when they don't need to cross one, and put a CDN in front of the bytes that do have to go out. None of that requires a reserved-instance spreadsheet. It requires actually reading the line nobody reads.