Why NAT Gateway Bills Are Shocking
NAT Gateway pricing has two components: $0.045/hour per NAT Gateway (about $32/month) and $0.045/GB of data processed. The hourly cost is predictable. The data processing cost is not.
Consider a Kubernetes cluster pulling Docker images from ECR during deployments. Each 500MB image pull Γ 50 pods deploying daily = 25GB/day = 750GB/month of NAT Gateway traffic = $33.75/month per cluster β on top of the $32/month for the NAT Gateway itself, on top of the ECR data transfer fees. For a company running 10 clusters, that is $657/month just from image pulls.
The solution is VPC Endpoints, which route traffic from your VPC directly to AWS services without going through NAT Gateway at all.
VPC Gateway Endpoints (Free)
# Gateway endpoints are FREE β no per-GB charge
# Supported services: S3 and DynamoDB ONLY
# Terraform: Create S3 and DynamoDB Gateway Endpoints
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,
aws_route_table.private_c.id,
]
policy = jsonencode({
Statement = [{
Action = "s3:*"
Effect = "Allow"
Resource = "*"
Principal = "*"
}]
})
tags = { Name = "s3-endpoint" }
}
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = [
aws_route_table.private_a.id,
aws_route_table.private_b.id,
aws_route_table.private_c.id,
]
tags = { Name = "dynamodb-endpoint" }
}
# After creating these, ALL S3 and DynamoDB traffic from private subnets
# automatically routes through the endpoint β zero NAT Gateway charges.
VPC Interface Endpoints (Paid but Worth It)
# Interface endpoints cost $0.01/hr per AZ = $7.20/month per AZ
# But eliminate NAT processing fees for all covered services
# High-value interface endpoints for most environments:
resource "aws_vpc_endpoint" "ecr_api" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
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
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ecs" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.ecs"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "secretsmanager" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.secretsmanager"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
resource "aws_vpc_endpoint" "ssm" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.ssm"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
}
resource "aws_security_group" "vpc_endpoints" {
name = "vpc-endpoints"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
}
Cost Break-Even Analysis for Interface Endpoints
Interface Endpoint Cost vs NAT Gateway Savings:
Setup: 3 AZs, 1 endpoint per service
Endpoint cost (ECR API + ECR DKR + ECS = 3 endpoints Γ 3 AZs):
9 Γ $0.01/hr Γ 720hr = $64.80/month
NAT Gateway savings from ECR traffic:
Assume 10 ECS tasks Γ 1GB image Γ 2 deploys/day Γ 30days = 600GB/month
NAT saving: 600GB Γ $0.045 = $27/month ... not enough
Better scenario (50 pods Γ 500MB Γ 3 deploys/day Γ 30days):
= 2,250GB/month NAT saving: 2,250 Γ $0.045 = $101.25/month
Endpoint cost: $64.80/month
Net saving: $36.45/month
Rule of thumb: Interface endpoints pay off when:
Monthly data volume through endpoint > (endpoint_count Γ AZ_count Γ $7.20) / $0.045
For 3 endpoints Γ 3 AZs: > $64.80 / $0.045 = 1,440 GB/month
For high-traffic production clusters (EKS, ECS):
Typically 5,000-50,000+ GB/month through ECR/S3 β big savings
Smaller clusters: gateway endpoints (S3, DynamoDB) are always worth it
Finding Your Current NAT Gateway Data Processing Costs
# Check NAT Gateway data processing costs in Cost Explorer
aws ce get-cost-and-usage --time-period Start=2024-01-01,End=2024-02-01 --granularity MONTHLY --filter '{
"And": [
{"Dimensions": {"Key": "SERVICE", "Values": ["Amazon Virtual Private Cloud"]}},
{"Dimensions": {"Key": "USAGE_TYPE_GROUP", "Values": ["VPC: NAT Gateway - Data Processed"]}}
]
}' --metrics "BlendedCost" --query 'ResultsByTime[0].Total.BlendedCost'
# Find which VPCs have NAT Gateways
aws ec2 describe-nat-gateways --query 'NatGateways[?State=='available'].{ID:NatGatewayId,VPC:VpcId,AZ:SubnetId}' --output table
# Check if S3/DynamoDB gateway endpoints already exist
aws ec2 describe-vpc-endpoints --filters "Name=vpc-endpoint-type,Values=Gateway" --query 'VpcEndpoints[*].{Service:ServiceName,State:State,VPC:VpcId}'
IPv6: Eliminating NAT Gateway Entirely
# IPv6 public addresses are free and don't need NAT Gateway
# EKS nodes with IPv6 can reach the internet directly
# Enable IPv6 on VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
assign_generated_ipv6_cidr_block = true
enable_dns_hostnames = true
enable_dns_support = true
}
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 1)
assign_ipv6_address_on_creation = true # All ENIs get IPv6 automatically
availability_zone = "us-east-1a"
}
# Egress-only internet gateway (IPv6 equivalent of NAT β but FREE)
resource "aws_egress_only_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.main.id
}
# Keep NAT for IPv4 external services that don't have IPv6
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
}
# EKS IPv6 cluster (fully dual-stack)
resource "aws_eks_cluster" "main" {
name = "production"
role_arn = aws_iam_role.eks.arn
kubernetes_network_config {
ip_family = "ipv6"
service_ipv6_cidr = null # AWS assigns automatically
}
vpc_config {
subnet_ids = aws_subnet.private[*].id
}
}
Summary: NAT Gateway Cost Reduction Playbook
Action | Cost | Saving | Effort
--------------------------------|-------------|------------------|--------
S3 gateway endpoint | FREE | All S3 NAT fees | 10 min
DynamoDB gateway endpoint | FREE | All DDB NAT fees | 10 min
ECR interface endpoints | $21.60/mo | Image pull fees | 30 min
Secrets Manager endpoint | $21.60/mo | Secret fetch fees| 30 min
SSM endpoint | $21.60/mo | SSM API fees | 30 min
Remove unused NAT Gateways | -$32/mo each| Direct saving | 30 min
IPv6 for internal services | FREE | Most IPv6 traffic| 1-2 days
Single NAT per region (dev) | -$64/mo | Direct saving | 1 hour
Typical result for a mid-size EKS cluster:
Before: $800/month (NAT hourly + data processing)
After gateway endpoints + ECR: $180/month
Monthly saving: $620 (77%)
Conclusion
NAT Gateway costs are one of the most impactful quick wins in AWS cost optimization. Gateway endpoints for S3 and DynamoDB are completely free and take 10 minutes to deploy β there is no reason not to have them. Interface endpoints for ECR pay for themselves in almost every EKS or ECS production environment.
After implementing all the strategies above, most organizations reduce NAT Gateway spend by 60-80%. The remaining NAT traffic is for external internet access that genuinely needs NAT, which is a legitimate cost you cannot eliminate without rearchitecting.
Marcus Rodriguez
Lead DevOps Engineer specializing in CI/CD pipelines, container orchestration, and infrastructure automation.