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

  1. Enable VPC Flow Logs to identify top traffic destinations.
  2. Create VPC Gateway Endpoints for S3 and DynamoDB immediately — these are always free.
  3. Create one NAT Gateway per AZ if you don’t already have them.
  4. Update route tables so each AZ’s private subnets route to the local NAT Gateway.
  5. For ECR pulls, create a VPC Interface Endpoint for ECR (costs apply but saves NAT charges).
  6. 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.