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.
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.
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.
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.
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 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.
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.
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 latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Everyone says Compose is for dev only. We ran it in production for two years on a single node and it was the right call, until the day it very much wasn't.
The top model on the MTEB leaderboard made our search worse and our bill bigger. Here's how we actually picked an embedding model for a real RAG system.
Explore more articles in this category
We ran secrets three different ways across AWS, GCP, and Vault. External Secrets Operator gave us one Kubernetes-native workflow. Here's the setup and the gotchas.
Moving our fleet from x86 to Graviton promised 20% savings. We got 31%, but only after fixing native dependencies, a broken base image, and one nasty perf regression.
A p99 that jumped to 3.4 seconds during traffic ramps turned out to be cold starts. Here's how we measured them properly and cut the tail, with real init timings.
Evergreen posts worth revisiting.