Last month, my AWS bill showed a NAT Gateway data processing charge that was nearly 40% higher than expected. I had one NAT Gateway in a single availability zone, and when I dug into the CloudWatch metrics, the data throughput seemed excessive for what my applications were actually doing. It turned out I was routing S3, DynamoDB, and ECR traffic through the NAT Gateway when they could have been completely free via VPC endpoints. In this post, I’ll walk through exactly what causes this and how to fix it.
The Problem
Your AWS bill shows surprisingly high NAT Gateway charges. You’re being charged for data processing ($0.045/GB in most regions), and when you check the logs, the data volume doesn’t match what you expected your applications to be transferring. A single NAT Gateway can easily cost $100+ per month if it’s handling gigabytes of traffic.
Here’s what unexpected NAT Gateway charges typically look like:
AWS Charges
NAT Gateway Processing: 250 GB × $0.045 = $11.25/day = $337.50/month
NAT Gateway Hours: 720 hours × $0.045 = $32.40/month
Total NAT Gateway Cost: ~$370/month
| Cost Driver | Impact |
|---|---|
| S3 Traffic via NAT | Every GB of S3 egress is charged at $0.045/GB |
| DynamoDB via NAT | DynamoDB operations routed through NAT cost data processing fees |
| ECR Image Pulls | Large container image pulls (500MB+) cost $0.045/GB |
| Cross-AZ NAT Usage | Instances in AZ-B using NAT Gateway in AZ-A: doubled costs |
Why Does This Happen?
-
S3 and DynamoDB traffic routing through NAT Gateway: By default, instances in private subnets route all traffic through the NAT Gateway, including AWS API calls. S3 GetObject calls and DynamoDB scans go through NAT, costing $0.045/GB when they could be free via VPC Gateway Endpoints.
-
ECR image pulls via NAT: When you’re pulling container images from ECR, those gigabytes of image data flow through the NAT Gateway. A single 2GB image pull costs $0.09. With multiple deployments daily, costs add up fast.
-
Cross-availability zone NAT usage: If you deployed a single NAT Gateway in AZ-A but have instances in AZ-B and AZ-C, all their traffic crosses AZ boundaries. AWS charges $0.01/GB for cross-AZ data transfer on top of NAT processing charges.
-
EC2 instances downloading large packages: Application servers and Lambda function layers downloading dependencies (npm packages, Python wheels, system updates) route through NAT. A 500MB package deployment costs $0.0225 per instance.
-
Unoptimized route tables: No VPC Gateway Endpoints configured, so even AWS API calls you could make privately are exiting through the NAT Gateway.
The Fix
First, check what’s actually using your NAT Gateway with VPC Flow Logs:
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0a1b2c3d4e5f6g7h8 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /aws/vpc/flowlogs \
--deliver-logs-permission-role-arn arn:aws:iam::123456789012:role/flowlogsRole
Query the logs to find the top traffic destinations:
aws logs filter-log-events \
--log-group-name /aws/vpc/flowlogs \
--filter-pattern "[version, account_id, interface_id, srcaddr, dstaddr, srcport, dstport, protocol, packets > 1000, bytes > 100000, windowstart, windowend, action, tcpflags]" \
--query 'events[*].message' \
--output text | awk '{print $5}' | sort | uniq -c | sort -rn
Create VPC Gateway Endpoints (Free)
Create a VPC Gateway Endpoint for S3:
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0a1b2c3d4e5f6g7h8 \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-0a1b2c3d4e5f6g7h8 rtb-0x1y2z3a4b5c6d7e8 \
--vpc-endpoint-type Gateway
Create a VPC Gateway Endpoint for DynamoDB:
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0a1b2c3d4e5f6g7h8 \
--service-name com.amazonaws.us-east-1.dynamodb \
--route-table-ids rtb-0a1b2c3d4e5f6g7h8 rtb-0x1y2z3a4b5c6d7e8 \
--vpc-endpoint-type Gateway
Verify the routes were added automatically:
aws ec2 describe-route-tables \
--route-table-ids rtb-0a1b2c3d4e5f6g7h8 \
--query 'RouteTables[0].Routes' \
--output table
Deploy NAT Gateway Per Availability Zone
Delete the single NAT Gateway and deploy one in each AZ:
aws ec2 describe-nat-gateways \
--filters "Name=vpc-id,Values=vpc-0a1b2c3d4e5f6g7h8" \
--query 'NatGateways[*].[NatGatewayId,SubnetId,State]'
Create a new NAT Gateway in each AZ (allocate Elastic IPs first):
aws ec2 allocate-address --domain vpc --output text
aws ec2 create-nat-gateway \
--subnet-id subnet-0a1b2c3d4e5f6g7h8 \
--allocation-id eipalloc-0a1b2c3d4e5f6g7h8
Update each private subnet’s route table to point to its local AZ NAT Gateway.
How to Run This
- Enable VPC Flow Logs to identify top traffic destinations.
- Create VPC Gateway Endpoints for S3 and DynamoDB immediately — these are always free.
- Create one NAT Gateway per AZ if you don’t already have them.
- Update route tables so each AZ’s private subnets route to the local NAT Gateway.
- For ECR pulls, create a VPC Interface Endpoint for ECR (costs apply but saves NAT charges).
- Monitor NAT Gateway metrics weekly:
aws cloudwatch get-metric-statistics --namespace AWS/NatGateway --metric-name BytesOutToDestination.
Is This Safe?
Yes, VPC Gateway Endpoints are safe and recommended. They don’t require changes to your applications—traffic automatically uses the endpoint once the route is added. Using per-AZ NAT Gateways is also safe; it’s just best practice.
Key Takeaway
NAT Gateway costs spike when you route AWS API traffic (S3, DynamoDB, ECR) through it. VPC Gateway Endpoints for S3 and DynamoDB are completely free and should be in every VPC. Deploy one NAT Gateway per AZ to avoid cross-AZ charges and improve availability.
Have questions or ran into a different networking issue? Connect with me on LinkedIn or X.