Viktar Patotski ·
· Cloud & Cost
· 12 min read
AWS Data Transfer Costs: Where the Money Leaks and How to Cut It
AWS charges to move bytes based on where they go. The expensive surprises are not the internet egress everyone expects, they are the cross-AZ chatter you cannot see. Here is the full rate ladder and the cheapest ways to cut the bill.
TL;DR - AWS charges to move bytes based on where the bytes go. Inbound from the internet is free. Traffic between machines in the same Availability Zone over private IPs is free. Everything else has a meter:
- Out to the internet: about $0.09 per GB (first 100 GB a month free, then it tiers down at volume).
- Cross-AZ in the same region: AWS bills both ends of the same transfer. The sender pays $0.01 per GB to send and the receiver pays $0.01 per GB to receive, so a single one-way gigabyte costs $0.02, not $0.01.
- Cross-region: about $0.01 to $0.02 per GB, billed on the sending side only (ingress to the destination region is free).
The expensive surprise is rarely the internet egress everyone expects. It is the cross-AZ chatter between your own services that no console screen shows you. Fixes, cheapest first: keep chatty pairs in the same AZ, use private IPs, route S3 and DynamoDB through free gateway endpoints, and put heavy internet egress behind CloudFront. Size it in Cost Explorer, find it in VPC Flow Logs.
The line item you cannot read
Data transfer is the AWS charge that defeats people. EC2 has an instance you
can point at. S3 has buckets. Data transfer is a tax on movement, spread across
half the services on your bill, reported as cryptic usage types like
DataTransfer-Regional-Bytes, with no single resource to click on. So when the
number is a few hundred dollars a month, most teams shrug and move on.
That is the mistake. Data transfer overspend is almost always architectural, which means it recurs every month and compounds as you scale. And the worst offender is invisible: traffic between your own services, in your own region, that you never thought of as “transfer” at all.
Here is the whole pricing model in one mental move, then where it leaks and how to cut it.
What AWS charges to move a byte
There is one question that prices every byte: where is it going? Three bands, cheapest to most expensive (us-east-1 rates, which are representative).
Free.
- Data in from the internet. Always free, every region.
- Traffic between resources in the same Availability Zone over private IPv4 addresses. An app talking to a database in the same AZ on private IPs: free.
- EC2 to S3 in the same region. Backups, asset reads, data pipelines that stay in-region pay nothing for the transfer itself.
Cheap, but it adds up.
- Cross-AZ in the same region: $0.01 per GB, and AWS meters both ends of the same transfer. Send one gigabyte from a service in AZ-a to a service in AZ-b and the sending resource is billed $0.01 for egress while the receiving resource is billed $0.01 for ingress: two line items, one transfer, $0.02 for that single one-way gigabyte. This is not a round trip. One GB moving in one direction is already billed twice, once on each side. (AWS calls it “$0.01/GB in each direction,” which hides that both directions apply to the same byte.) Applies to EC2, RDS, Redshift, ElastiCache, and more.
- Cross-region: roughly $0.01 to $0.02 per GB depending on the pair, and here only the sending side is charged. Data into the destination region is free; the bill is the egress from the source region. Same-continent hops (us-east-1 to us-west-2) sit around $0.02; longer hauls cost more.
Expensive.
- Out to the internet: about $0.09 per GB in us-east-1 for the first 10 TB a month, after a 100 GB monthly free allowance that is shared across all services and regions. It tiers down with volume ($0.085 for the next 40 TB, $0.07 for the next 100 TB, $0.05 above 150 TB), but most accounts live in that first $0.09 tier.
That is the whole model. The bill is just these three bands multiplied by how many bytes take each path. Optimizing data transfer is moving bytes from an expensive band to a cheaper one, or to free.
Where the money leaks
1. Cross-AZ chatter you cannot see
This is the big one, and it is invisible. Spread your services across Availability Zones for resilience (the right call), and every gigabyte that crosses a zone boundary now costs $0.02, because AWS bills the sender and the receiver for the same transfer. Microservices calling each other, an app reading a database replica in another AZ, a Kafka consumer reading from a broker in another zone, a cache in one AZ serving an app in another: all of it meters, on both ends, all day.
Two things make it sting. The rate looks like $0.01 in the pricing table, but you pay it twice because both the sender and the receiver are billed. And there is no “cross-AZ transfer” resource in the console to look at, so the cost hides inside a regional-bytes usage type that nobody opens. A chatty service mesh can quietly push terabytes a month across AZ lines.
2. AWS-service traffic taking the expensive path
Traffic to S3, DynamoDB, and other AWS services does not have to touch the internet meter, but by default some of it does. If instances in a private subnet reach S3 through a NAT gateway, you pay the NAT processing fee on top of everything else. If they reach AWS services over public IP addresses, that can attract regional transfer charges it did not need to. This is the same leak the NAT gateway post covers in depth, and the fix is the same: keep that traffic on private, in-region paths.
3. Internet egress at full price when CloudFront is cheaper
Serving files, API responses, or media straight out of EC2 or S3 to users pays the $0.09 per GB internet rate. Put a CloudFront distribution in front and two things change: the transfer from your origin (S3 or EC2) into CloudFront is free, and CloudFront’s own egress to users is billed at its rate with a larger free tier (1 TB a month). For anything served repeatedly to many users, routing egress through CloudFront cuts the per-GB rate and adds caching on top.
4. Cross-region traffic you did not design
Cross-region replication, a service in one region calling a database in another, multi-region setups stitched together without a transfer budget: each hop is $0.01 to $0.02 per GB and runs continuously. Multi-region is sometimes necessary. Paying for it by accident, because a resource landed in the wrong region, is not.
The fixes, cheapest first
Co-locate chatty pairs in the same AZ (free)
If two services talk constantly, put them in the same Availability Zone and let them speak over private IPs. Same-AZ private traffic is free, so the $0.02 per GB (both ends billed) drops to zero. The tension is real: same-AZ placement trades a little zone-failure resilience for cost. The answer is not “always co-locate,” it is “co-locate the chatty pairs where the transfer cost outweighs the redundancy you actually need.” A high-volume app-to-cache or app-to-replica link is a strong candidate. Your primary database failover path is not.
Read from the nearest replica, not across the zone
Some workloads cannot co-locate. A Kafka or database cluster is spread across AZs on purpose for durability, and the clients are spread too. The bytes still have to move, but they do not all have to cross a zone.
Kafka on Amazon MSK is the clearest example, and a common one on EKS. By default a consumer reads from the partition leader, and leaders are balanced across AZs, so in a three-AZ cluster roughly two-thirds of your consume traffic crosses a zone boundary and bills at $0.02 per GB. One thing MSK does not charge for is the broker-to-broker replication that keeps the cluster in sync: that part is free. It is your consumers and producers crossing zones that meters.
The fix is rack awareness, Kafka’s nearest-replica fetching. Set each consumer’s
client.rack to the AZ its pod runs in, and MSK serves the read from an in-sync
replica in that same AZ even when the leader lives elsewhere. The cross-AZ
consume traffic drops, by up to two-thirds in AWS’s own measurements. On EKS you
feed client.rack from the node’s AZ label through the Downward API. Producers
still write to the leader, so the produce side keeps its cross-AZ cost, but on a
read-heavy stream the consumer side is where the bill is.
The principle generalizes: before you accept a cross-AZ charge as unavoidable, check whether the service can hand the client a same-AZ copy. Managed data services increasingly can.
Use private IPs, not public ones
Same-AZ traffic is free only over private IP addresses. The moment two resources talk over a public or Elastic IP, AWS charges $0.01 per GB in and $0.01 per GB out, even when they sit in the same Availability Zone. A service reaching a sibling through a public load balancer or a public DNS name can pay $0.02 per GB for a hop that is free over private addressing. Keep internal traffic on private VPC addresses and it stays on the free or cheap internal routes instead of looping out and back.
Gateway endpoints for S3 and DynamoDB (free)
A gateway VPC endpoint routes S3 and DynamoDB traffic privately inside AWS with no hourly charge and no data processing charge. It keeps that traffic off the NAT gateway and off any public path. If your workloads touch S3 at all, this is a free change with immediate effect. (Full detail in the NAT gateway post.)
CloudFront for heavy internet egress
For content served to users repeatedly, put CloudFront in front of the origin. Origin-to-CloudFront transfer is free, CloudFront egress has a 1 TB monthly free tier, and the cache cuts how often you hit the origin at all. This is the lever for media, downloads, and static assets, not for low-volume API traffic where the setup is not worth it.
Compress and batch
The cheapest byte is the one you do not send. Gzip or Brotli on API responses and asset delivery, batched writes instead of chatty per-row calls across an AZ boundary, and trimming oversized payloads all cut the GB count directly. On a high-volume cross-AZ path, compression alone can take a real bite out of the $0.02 per GB.
Find it: Cost Explorer sizes it, Flow Logs name it
Two tools, two jobs, and people conflate them.
Cost Explorer tells you how big the transfer bill is. Group by usage type
and you see the lines: DataTransfer-Out-Bytes for internet egress,
DataTransfer-Regional-Bytes for cross-AZ, the inter-region usage types for
cross-region. That answers “is this worth chasing and which band is it in?” It
does not tell you which services or instances generated the bytes. There is
no source-and-destination breakdown in the cost data.
VPC Flow Logs tell you what the traffic was. Turn on Flow Logs for the VPC or subnet, then query them with Athena or CloudWatch Logs Insights, grouping by source and destination address and summing bytes. Now you can see that this much cross-AZ traffic is one service hammering a replica in another zone, and that much egress is one endpoint serving uncached media. Cost Explorer says “your regional transfer is $300 a month.” Flow Logs say “and 80% of it is the order service talking to a cache in the wrong AZ.”
Do the math on your own account
The pricing is public, so you can size a leak before touching anything. Take a pair of services that exchange 10 TB a month across an AZ boundary, a normal volume for a busy internal API:
- Cross-AZ at $0.02 per GB (both ends billed): about 10,000 GB times $0.02, roughly $200 a month, for traffic that never leaves your region and shows up under no resource you would think to look at.
- Move that pair into the same AZ on private IPs and the same traffic is free.
That is one pair. Multiply across a service mesh and the cross-AZ line is often larger than the internet egress everyone worries about. The exact split depends on your traffic mix, which is the point: size each band in Cost Explorer first, use VPC Flow Logs to find which paths generate it, then move the biggest ones to a cheaper band. Most of the work is reading the bill and the logs, not changing infrastructure.
Summary
AWS prices every byte by where it goes: free inbound and same-AZ private, cheap cross-AZ and cross-region, expensive out to the internet. The bill that surprises people is rarely the internet egress they expect. It is the cross-AZ chatter between their own services, billed on both ends, hidden in a usage type nobody opens. Co-locate the chatty pairs, keep internal traffic on private IPs, route S3 and DynamoDB through free gateway endpoints, and put heavy egress behind CloudFront. Size the bands with Cost Explorer, name the contributors with VPC Flow Logs, then move the biggest ones. It is an afternoon against a charge that recurs every month.
The closest sibling to this one is the NAT gateway, the other place network bytes quietly cost you: see the NAT gateway cost trap. For the database side of the bill, see how to reduce AWS RDS costs without hurting performance.
Want someone to find these in your account? I do this kind of work as part of AWS Cost Optimization. Book a free 30-minute call and I will show you where the waste is. Or grab the free AWS cost checklist and find the quick wins yourself.