Cross-AZ traffic is billed twice for one transfer, once on the way out and once on the way in. It is the only AWS network charge where the sender and the receiver both pay, and it is the reason a three-zone deployment can cost more in transfer than in compute without a single byte leaving the Region.
How does AWS charge for data transfer between Availability Zones?
From the "Data Transfer within the same AWS Region" section of the EC2 on-demand pricing page:
Data transferred "in" to and "out" from Amazon EC2, Amazon RDS, Amazon Redshift, Amazon DynamoDB Accelerator (DAX), Amazon ElastiCache instances, and Elastic Network Interfaces across Availability Zones in the same AWS Region is charged at $0.01/GB in each direction.
In each direction. Move a gigabyte from an instance in us-east-1a to an instance in us-east-1b and you pay $0.01 on the sending side and $0.01 on the receiving side. The effective rate is $0.02 per GB, which is comparable to internet egress in some Regions, for traffic that never leaves the building.
| Path | Charged? | Source |
|---|---|---|
| Between EC2, RDS, Redshift, DAX, ElastiCache or ENIs in the same AZ | Free | "Data transferred between Amazon EC2, Amazon RDS, Amazon Redshift, Amazon ElastiCache instances, and Elastic Network Interfaces in the same Availability Zone is free." |
| Across AZs in the same Region | $0.01/GB each way | EC2 on-demand pricing, quoted above |
| Via a public or Elastic IPv4 address, even in the same AZ | $0.01/GB each way | "IPv4: Data transferred 'in' to and 'out' from public or Elastic IPv4 address is charged at $0.01/GB in each direction." |
| Via an IPv6 address in a different VPC | $0.01/GB each way | "IPv6: Data transferred 'in' to and 'out' from an IPv6 address in a different VPC is charged at $0.01/GB in each direction." |
| VPC peering that stays inside one AZ | Free, including across accounts | Demystifying VPC peering charges |
| S3, DynamoDB, SQS, SNS, Kinesis, ECR to EC2 in the same Region | Free, if nothing is in the path | EC2 on-demand pricing, "Data transfer within the same AWS Region" |
Why does using a public IP cost money inside one zone?
This is the row people do not believe. The pricing page charges traffic through a public or Elastic IPv4 address at $0.01/GB each way with no Availability Zone condition attached, unlike the line above it. Two instances sitting in the same subnet, talking over their public addresses because somebody hardcoded a DNS name that resolves publicly, are paying for a transfer that would be free over private addresses.
The EC2 instance IP addressing documentation states the same thing from the other direction: "Instances that access other instances through their public NAT IP address are charged for regional or Internet data transfer, depending on whether the instances are in the same Region." The fix costs nothing. Use private DNS. It also removes the $0.005 per hour those addresses are costing you.
What about traffic to S3 and DynamoDB?
Free in-Region, with a caveat that undoes it for most private subnets. The pricing page grants the exemption and then withdraws it in the same sentence:
Data transferred directly (see endpoints) between Amazon S3, Amazon EBS direct APIs, Amazon Glacier, Amazon DynamoDB, Amazon SES, Amazon SQS, Amazon Kinesis, Amazon ECR, Amazon SNS or Amazon SimpleDB and Amazon EC2 instances in the same AWS Region is free. If other AWS services are in the path of your data transfer, you will be charged their associated data processing costs. These services include, but are not limited to, PrivateLink endpoints, NAT Gateway and Transit Gateway.
"Directly" is doing the work. An instance in a private subnet reaching S3 through a NAT Gateway is not direct, so the transfer is free and the $0.045 per GB of NAT processing is not. A gateway VPC endpoint restores directness and carries no charge at all, which is why it is the least ambiguous saving in AWS networking. An interface endpoint does not: it is a PrivateLink endpoint, and PrivateLink is named in that list.
How do you check AWS data transfer in your bill?
Cross-AZ traffic has one usage type. The
AWS reference on understanding data transfer charges
defines it as {{Region}}-DataTransfer-Regional-Bytes, "For
example, the USE2-DataTransfer-Regional-Bytes usage type
identifies charges for data transfer between Availability Zones in the US East
(Ohio) Region."
The same page confirms the double billing at the line-item level: "For a given
resource, you're charged for both inbound and outbound traffic in a data
transfer within an AWS Region. This means for each resource metered, you'll
see two DataTransfer-Regional-Bytes line items for each data
transfer."
Three traps in that naming, all documented, all of which we have hit while writing queries against a customer lake.
| Trap | What AWS says |
|---|---|
| A usage type with no Region prefix is not a global total | "Data transfer usage types that don't have the Region prefix, such as DataTransfer-Regional-Bytes or DataTransfer-Out-Bytes, represent data transfer from the US East (N. Virginia) Region." |
| Inter-Region transfer is charged once, not twice | "There's no charge for the data transferred into the destination Region. The data transfer charge is determined by the data transferred out from the source Region." |
| VPC peering moved out of this usage type in April 2025 | "Starting in April 2025, the VPC Peering charges for IntraRegion are reflected under the usage types ... USE1-VpcPeering-In-Bytes, and USE1-VpcPeering-Out-Bytes. Prior to April 2025, these charges were recorded under the usage type DataTransfer-Regional-Bytes." |
That last one silently breaks old queries. A dashboard written in 2024 that
matched %DataTransfer-Regional-Bytes% and called the result
"cross-AZ" started under-reporting in April 2025, and it did not fail. It
returned a smaller number. This is the failure mode that makes CUR queries
dangerous: they go wrong quietly. Match both patterns:
SELECT line_item_usage_type,
line_item_usage_account_id,
SUM(line_item_unblended_cost) AS cost,
SUM(line_item_usage_amount) AS gb
FROM cur
WHERE billing_period = '2026-07'
AND (line_item_usage_type LIKE '%DataTransfer-Regional-Bytes'
OR line_item_usage_type LIKE '%VpcPeering-%-Bytes')
GROUP BY 1, 2
ORDER BY cost DESC;
Setting that table up without a crawler is covered in querying your CUR in Athena with partition projection, and if you would rather not scan the whole lake to find out, the per-query data scanned limit is the control that actually stops a runaway query.
Where does cross-AZ traffic actually come from?
Almost never from the thing people suspect. In the accounts we have looked at, the volume concentrates in a short list.
- Kubernetes service traffic. A Service with default settings load balances across every ready endpoint regardless of zone, so roughly two-thirds of pod-to-pod calls in a three-zone cluster cross a boundary by design. This is the single largest source we see, and it does not show up as an EKS charge. It shows up under EC2 - Other, unattributed, which is exactly the gap split cost allocation data does not fill: SCAD splits instance cost, not network cost.
- Replicas in another zone. A Multi-AZ RDS deployment, a cross-zone Kafka cluster or an ElastiCache replication group pays $0.02 per GB on every byte of replication traffic, permanently. That is the price of the availability, not a mistake, but it should be a budgeted number rather than a surprise.
- Cross-zone load balancing. Distributing evenly across zones is the default behaviour people want, and it guarantees the boundary is crossed for most requests.
- Logging and metrics agents. High-volume shipping to a collector that lives in one zone. If you also ship VPC Flow Logs, you are paying to transport the record of the traffic as well as the traffic.
Zonal affinity is the lever for the first and third of those, and it is a configuration change rather than an architecture change. Availability is the reason to accept the charge on the second. Neither decision can be made until the number is visible, and by default it is not: it is aggregated into a bucket named after a service that did not generate it. Splitting it out is a group-by-usage-type away.