Networking — 11 min read
AWS data transfer costs explained
Nobody moves too much data by accident. They move a normal amount along a path that happens to be metered.
Data transfer is the only significant AWS charge with no console, no dashboard, and no service page. There is no "Data Transfer" in the services menu you can open to see what you're spending. It is billed as a side effect of things you did elsewhere, which is why it's simultaneously one of the largest lines on a mature bill and the one nobody can explain.
The good news is that the rules are simple, there aren't many of them, and once you know them the expensive shapes are obvious on sight.
Why you can't find it
Two accounting decisions make transfer nearly invisible.
It's billed under the service that caused it, not as its own line. Bytes leaving an EC2 instance are on the EC2 line. Bytes leaving S3 are on the S3 line. Bytes leaving through a load balancer are on the ELB line. So no single number in Cost Explorer ever represents "what we spent moving data" unless you deliberately assemble it.
The names are usage types, not English. The charge for traffic between
availability zones is called DataTransfer-Regional-Bytes. Nothing in that
string says "your application is talking to a database in another AZ, and you're
paying twice for every gigabyte of it."
The practical consequence: transfer costs don't get investigated when they start
— they get investigated years later, when someone finally asks what
EC2 — Other is.
The price of a gigabyte depends entirely on the path
Same bytes, same application, same region. The only variable is which boundary they cross.
That bottom bar is the one worth staring at. Traffic that leaves a private subnet for the internet through a NAT Gateway pays the NAT processing charge and the egress charge. They stack. It is the most expensive ordinary path in AWS, and it is also the default path, because a private subnet with a NAT Gateway is what every reference architecture draws.
The rules, in full
| Path | Charge | Notes |
|---|---|---|
| In from the internet | Free | Uploads cost nothing. |
| Out to the internet | ~$0.09/GB | First 100 GB/month free across the account. Tiers down above 10 TB. |
| Between AZs, same region | ~$0.01/GB each way | So $0.02 for a gigabyte that crosses and comes back. |
| Within one AZ, private IP | Free | The private IP part is not optional — see below. |
| Within one AZ, public or Elastic IP | ~$0.01/GB each way | Same rack, billed as if it left. |
| Between regions | ~$0.02/GB | Varies by pair; higher out of some regions. |
| NAT Gateway | ~$0.045/GB processed | Plus ~$0.045/hour per gateway, per AZ, idle or not. |
| S3 / DynamoDB gateway endpoint | Free | No hourly, no per-GB. |
| Interface endpoint (PrivateLink) | ~$0.01/hour/AZ + ~$0.01/GB | Cheaper than NAT for the same traffic, not free. |
| CloudFront to viewers | ~$0.085/GB | And AWS origin → CloudFront is free, which is the whole trick. |
| Transit Gateway | ~$0.02/GB + attachment hours | Charged per attachment, so hub-and-spoke adds up. |
Prices are us-east-1 list at time of writing and vary by region — check current pricing before you build a business case on a number here. The ratios are what matter, and they've been stable for years.
What your paths actually cost
Put in roughly what your account moves in a month. If you don't know, that's the finding — the last section shows you how to get real numbers in about ten minutes.
Charged on top of egress — traffic to the internet through NAT pays both.
3 gateways × 730 hours, whether or not a byte moves.
Billed in both directions, and invisible in Cost Explorer until you split by usage type.
The two red lines — $2,820 a year here — are the ones that are usually architecture rather than necessity. Endpoints and AZ-aware routing remove most of them without changing what your application does.
us-east-1 list prices at time of writing. Egress rates fall in tiers above 10 TB and vary by region; NAT hourly is per gateway, per AZ. Check current rates for your region.
The NAT hourly slider is the one that catches people out. Three gateways across three AZs is standard high-availability practice, and it's about $98 a month before a single byte moves. That's fine and correct for production. It is not fine for the four dormant dev VPCs that also have three each.
The five shapes that cost the most
1. Same-region S3 traffic going through NAT. An application in a private subnet reading from S3 has its traffic routed out through the NAT Gateway, processed at ~$0.045/GB, for data that never leaves the region. A VPC gateway endpoint for S3 makes that path free — no hourly charge, no per-GB charge. It's a route table entry, it takes minutes, and it is the single highest-return change in this entire guide. The same applies to DynamoDB. The NAT Gateway guide covers this one in depth, because it's usually the largest.
2. Chatty services in different AZs. An application tier in us-east-1a
talking to a database in us-east-1b pays ~$0.01/GB out and ~$0.01/GB back for
every query and every result set. For a busy service this is continuous, and the
architecture diagram that produced it looks completely reasonable — AZ spread is
what you're told to do. It is right for the database's replica placement; it
is not automatically right for every hop in between.
3. Private traffic taking the public path. If a service reaches another service inside your VPC by its public DNS name or Elastic IP, the traffic exits and re-enters, and is billed accordingly — even inside one AZ. This is almost always accidental: a config value copied from a laptop, a hostname that was public in staging. It's free to fix and invisible until you look.
4. Serving bytes to the internet directly. Static assets served from S3 or straight through an ALB pay full egress. Put CloudFront in front and the origin fetch becomes free, with the viewer-facing egress priced lower than direct S3. For any real volume of static content this is a straightforward reduction.
5. Cross-region replication nobody remembers. S3 Cross-Region Replication, cross-region snapshot copies, or a global database enabled for a compliance requirement that has since changed. These bill quietly and forever, and the decision that created them is usually two org charts old.
Decide
You've found a large transfer charge. What do you do first?
Finding yours, in about ten minutes
Step one — Cost Explorer, grouped by usage type. Not by service. Service tells you which team's budget it lands in; usage type tells you what physically happened. Filter to the last three months and look for these strings:
NatGateway-Bytes— traffic processed by NAT. The usual winner.NatGateway-Hours— gateways existing. Divide by 730 to count them.DataTransfer-Regional-Bytes— cross-AZ, in region. The invisible one.DataTransfer-Out-Bytes— egress to the internet.<REGION>-<REGION>-AWS-Out-Bytes— inter-region, one line per pair.
Those five strings account for the overwhelming majority of transfer spend in a typical account.
Step two — VPC Flow Logs, for the cross-AZ number only. Cost Explorer tells you how much crossed an AZ boundary; it cannot tell you which two things were talking. Flow logs can. Enable them to S3 for a week, query with Athena, group by source and destination, and the chatty pair is usually obvious in the first result row. Turn them off afterwards — flow logs at full volume are themselves a real CloudWatch cost, which is a mildly embarrassing way to increase a bill you were trying to reduce.
Step three — check your endpoints. In the VPC console, list endpoints per VPC. If a VPC has workloads using S3 or DynamoDB and no gateway endpoint, you've found paid traffic that should be free, and you don't need any further evidence than that.
The thing worth remembering
Almost every expensive transfer charge is a routing problem, not a volume problem. Nobody moves too much data by accident. They move a normal amount of data along a path that happens to be metered, chosen at design time by someone optimising for availability or for getting it working, both of which are correct priorities that simply don't include this.
That's also why it's worth fixing properly rather than repeatedly: an endpoint added once keeps being free, and a service moved next to the thing it talks to stays next to it.
- Why is my NAT Gateway bill so high? — the largest single component, in depth.
- Why did my AWS bill suddenly go up? — if transfer spiked on a specific date rather than growing.
- The AWS cost optimization checklist — where this sits relative to everything else worth doing.
If you'd rather see the split before spending an afternoon in Cost Explorer, the
free teardown reads an export in your browser and breaks out
EC2 — Other — the bucket where NAT and cross-AZ transfer both hide. No upload,
no signup, nothing leaves the page.
Check your own account
Does this apply to you? Find out in two seconds.
Drop a Cost Explorer export into the free teardown. It reads your service mix and tells you which of these patterns show up in your bill — in your browser, with no upload and no email.