NAT Gateway Costs: The Silent Line Item and How to Cut It
A single NAT Gateway quietly billed us $2,900 in one month, mostly for data processing on traffic that never needed to leave the VPC. Here's how we found it and cut it.
Key takeaways
- A single NAT Gateway quietly billed us $2,900 in one month, mostly for data processing on traffic that never needed to leave the VPC.
- Here's how we found it and cut it.
NAT Gateway Costs: The Silent Line Item and How to Cut It#
The finance team flagged it, not us. A line called "EC2 Other" on the AWS bill had crept to $2,900 in a month, and nobody could say what it was. It turned out to be a single NAT Gateway processing 31 TB of traffic. The kicker: most of that traffic was our services pulling objects from S3 and images from ECR, both of which live inside AWS. We were paying a per-gigabyte data processing fee to route AWS traffic to AWS, out through the internet and back.
Why the bill hides#
NAT Gateway has two charges that stack. There's an hourly cost, about $0.045 per hour per gateway, which is $32 a month and nobody notices. Then there's data processing at roughly $0.045 per GB. That second one is invisible because it shows up under "EC2 Other" on most cost reports, not under anything labeled "NAT." You have to go looking.
The first move is to confirm the volume with flow logs. Enable them on the NAT Gateway's ENI and query in Athena:
SELECT pkt_dstaddr,
SUM(bytes) / 1073741824.0 AS gib
FROM vpc_flow_logs
WHERE interface_id = 'eni-0a1b2c3d4e5f'
AND start >= 1719792000
GROUP BY pkt_dstaddr
ORDER BY gib DESC
LIMIT 20;
Our top destinations were all S3 and ECR endpoints. That's the tell. If your NAT traffic is dominated by AWS service IPs, you're paying data processing for nothing, because those services can be reached over VPC endpoints that skip NAT entirely.
Gateway endpoints for S3 and DynamoDB are free#
This is the highest-return change and it costs nothing. Gateway VPC endpoints for S3 and DynamoDB have no hourly fee and no data processing charge. You add a route and your S3 traffic stops touching the NAT Gateway.
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private_a.id,
aws_route_table.private_b.id]
}
That one resource took about 60 percent of our NAT volume off the meter immediately. S3 pulls now go straight from the private subnet to S3 over AWS's backbone.
Interface endpoints for everything else, with a catch#
ECR, Secrets Manager, STS, CloudWatch, and most other services use Interface endpoints (PrivateLink), which do have an hourly cost, around $0.01 per hour per endpoint per AZ, plus a small per-GB fee that's still well under NAT's rate. The math flips fast when volume is high.
For ECR you actually need three endpoints working together, and people miss this: ecr.api, ecr.dkr, and the S3 gateway endpoint, because image layers are stored in S3. Set up only the first two and pulls still hit NAT for the layers.
resource "aws_vpc_endpoint" "ecr_dkr" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
private_dns_enabled = true
security_group_ids = [aws_security_group.endpoints.id]
}
Run the numbers before you add every endpoint. An interface endpoint at $0.01/hr is about $7/month per AZ. If a service only pushes 2 GB a month through NAT, the endpoint costs more than the NAT processing. We only added endpoints where monthly volume through NAT exceeded roughly 150 GB.
The cross-AZ trap#
The other quiet cost is placement. If your NAT Gateway sits in AZ us-east-1a and an instance in us-east-1b uses it, you pay NAT processing plus cross-AZ data transfer at $0.01/GB each way. On chatty workloads that doubles fast. The fix is one NAT Gateway per AZ with each private subnet routed to the gateway in its own zone. You pay more in hourly fees (three gateways instead of one) but you stop paying cross-AZ transfer, and on any real volume that wins.
Where we landed#
After the S3 and ECR endpoints plus per-AZ placement, that $2,900 gateway dropped to about $340 the next month. The remaining traffic was genuine outbound internet: third-party APIs, package registries, webhook deliveries. That's the traffic NAT is actually for, and we stopped fighting it.
The call we'd make#
Add the S3 and DynamoDB gateway endpoints today, in every VPC, no analysis needed, they're free and they only help. Then pull flow logs, find your top NAT destinations, and add interface endpoints for any AWS service pushing more than ~150 GB a month. Put a NAT Gateway in each AZ if you have cross-AZ chatter. Do those three things and the silent line item usually shrinks by 80 percent or more. The traffic left over is the traffic you're supposed to be paying for.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Terraform Import at Scale: Bringing Legacy Infra Under Code
We inherited 200-odd AWS resources built by hand over four years, with no state file anywhere. Here's how import blocks and a generation workflow got them under Terraform without a rebuild.
GitHub Actions Reusable Workflows: DRY Pipelines at Org Scale
We had the same 180-line build workflow copy-pasted into 60 repos. Fixing one bug meant 60 PRs. Here's the reusable-workflow setup that made it one.
More from Cloud
Explore more articles in this category
Azure OpenAI's Sweden Central Outage: A Health Check Postmortem
A slow database made a backend service look dead, and the health check that was supposed to protect it killed it faster. The lesson has nothing to do with AI.
GKE Pod Snapshots: Cold Starts Drop 89%, If Your Nodes Match
Google's benchmarks show a 70B model restoring in 37 seconds instead of minutes. The catch is a hash and a hardware match that silently refuses to restore when either is off.
AWS Lost a Region for Good: Multi-AZ Is Not Disaster Recovery
AWS says it cannot restore data held only in Bahrain (me-south-1) or in one UAE zone. Multi-AZ gave availability, not recovery, and only cross-region copies survived.
You might have missed
Evergreen posts worth revisiting.