Journal / Technical notes

Eliminate AWS NAT Gateway Overcharges: Terraform Guide

NAT Gateway fees quietly become a top AWS line item. Route S3, ECR and DynamoDB traffic through VPC Endpoints instead, with copy-paste Terraform.

On this page

TL;DR

If you are transferring gigabytes of data between your private compute nodes and AWS services like ECR, S3, or DynamoDB, you are likely paying a massive premium on NAT Gateway Data Processing charges ($0.045 per GB).

  • The Solution: Replace public NAT Gateway paths with private VPC Endpoints (AWS PrivateLink).
  • The Savings: Removes the $0.045/GB data processing charge for S3 and DynamoDB traffic entirely, and cuts it to about $0.01/GB for high-volume services like ECR. When those services carry most of your NAT traffic, most of the NAT bill goes with them.
  • The Code: Drop-in Terraform modules for Gateway Endpoints (free) and Interface Endpoints are detailed below.

Is your AWS bill out of control?

Book a free 15-minute AWS bill review and I'll tell you where the data transfer charges are leaking, or get the $250 fixed-price AWS Cost Audit for written findings plus the Terraform to fix them.


The Silent Killer: Data Processing Fees

When you configure a public NAT Gateway in AWS, you pay two different charges:

  1. The Hourly Charge: $0.045 per hour per NAT Gateway ($32/month).
  2. The Data Processing Charge: $0.045 per GB for all data passing through.

The hourly charge is negligible. The data processing charge is a financial landmine.

If your private EC2 instances or EKS pods pull a 2 GB Docker image from ECR, read 500 GB of backups from S3, or query a DynamoDB table, and that traffic goes through your NAT Gateway, you are paying $0.045/GB to route data that never leaves the AWS global network.

At 10 TB of monthly data transfer, you are burning $450/month on NAT Gateway tax. At 100 TB, that tax jumps to $4,500/month.

If you know roughly how many NAT Gateways you run and how much data crosses them, the AWS cost waste calculator will put a figure on it in a few seconds.

The Fix: VPC Endpoints

Instead of routing traffic to AWS services through a NAT Gateway, you can create VPC Endpoints. This exposes a private IP address inside your VPC subnet that connects directly to the target AWS service using the private AWS fiber network.

AWS offers two types of VPC Endpoints:

1. Gateway Endpoints (Free)

Available only for S3 and DynamoDB. They modify your subnet route tables to direct traffic privately.

  • Cost: 100% Free.
  • Data Processing Fee: $0.00.

2. Interface Endpoints (Paid)

Available for almost all other AWS services (ECR, KMS, Systems Manager, CloudWatch, API Gateway). They create an Elastic Network Interface (ENI) with a private IP in your subnets.

  • Cost: $0.01 per hour per availability zone ($7.20/month per AZ).
  • Data Processing Fee: ~$0.01 per GB (a 78% reduction compared to NAT Gateway's $0.045/GB).

Implementing with Terraform

Let's deploy these endpoints using Terraform to automate the cost optimization safely. The complete module, with variables and outputs, is on GitHub: aws-vpc-endpoints-terraform. Not sure the migration is worth it for your traffic? Run your numbers through the AWS cost waste calculator first.

Step 1: Deploy Free S3 and DynamoDB Gateway Endpoints

Since Gateway Endpoints are completely free and have no data processing charges, there is absolutely zero reason not to deploy them today.

vpc-gateway-endpoints.tf

# Retrieve the current VPC details
data "aws_vpc" "selected" {
  id = var.vpc_id
}
 
# Retrieve all private route table IDs where you want private routing
data "aws_route_table" "private" {
  count     = length(var.private_subnet_ids)
  subnet_id = var.private_subnet_ids[count.index]
}
 
locals {
  route_table_ids = distinct([for rt in data.aws_route_table.private : rt.id])
}
 
# Create S3 Gateway Endpoint
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = data.aws_vpc.selected.id
  service_name      = "com.amazonaws.${var.aws_region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = local.route_table_ids
 
  tags = {
    Name        = "s3-gateway-endpoint"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}
 
# Create DynamoDB Gateway Endpoint
resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = data.aws_vpc.selected.id
  service_name      = "com.amazonaws.${var.aws_region}.dynamodb"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = local.route_table_ids
 
  tags = {
    Name        = "dynamodb-gateway-endpoint"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

Step 2: Deploy ECR and CloudWatch Interface Endpoints

If you run EKS, ECR image pulls are typically your largest source of NAT Gateway processing fees. Routing ECR and CloudWatch traffic through private Interface Endpoints bypasses the NAT Gateway entirely.

vpc-interface-endpoints.tf

# Security Group to allow internal HTTPS traffic to the Endpoints
resource "aws_security_group" "vpc_endpoints" {
  name        = "${var.environment}-vpc-endpoints-sg"
  description = "Allow private traffic to VPC endpoints"
  vpc_id      = data.aws_vpc.selected.id
 
  ingress {
    description = "Allow HTTPS from within the VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [data.aws_vpc.selected.cidr_block]
  }
 
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
 
  tags = {
    Name        = "vpc-endpoints-sg"
    Environment = var.environment
  }
}
 
# ECR API Endpoint (for image lists and registry calls)
resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = data.aws_vpc.selected.id
  service_name        = "com.amazonaws.${var.aws_region}.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
 
  tags = {
    Name        = "ecr-api-endpoint"
    Environment = var.environment
  }
}
 
# ECR DKR Endpoint (for actual image layer pulls)
resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = data.aws_vpc.selected.id
  service_name        = "com.amazonaws.${var.aws_region}.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
 
  tags = {
    Name        = "ecr-dkr-endpoint"
    Environment = var.environment
  }
}

Free PDF checklist

The 10-point AWS cost checklist

The checks I run first on an AWS bill, each with the AWS CLI command that finds the waste and what to do about it. You can also read it on the site.


When Are Interface Endpoints NOT Worth It?

Interface Endpoints cost ~$7.20/month per availability zone. If you deploy an endpoint across 3 AZs, you are paying $21.60/month flat, plus $0.01/GB.

If a specific service (like KMS or Secret Manager) only processes 5 GB of traffic a month inside your VPC:

  • Through NAT Gateway: $0.22 in data processing.
  • Through VPC Endpoint: $21.60 in flat hourly fees + $0.05 in processing.

In this scenario, deploying the Interface Endpoint is actually more expensive than routing it through the NAT Gateway.

The break-even math: every GB moved to an Interface Endpoint saves $0.045 − $0.01 = $0.035. Divide the endpoint's monthly hourly cost by that:

  • 1 AZ: $7.20 ÷ $0.035 ≈ 206 GB/month
  • 3 AZs: $21.60 ÷ $0.035 ≈ 617 GB/month

Below that volume, leave the service on the NAT Gateway. Above it, the endpoint wins, and the gap grows with every GB.

The Payoff

By routing S3/DynamoDB traffic to Gateway Endpoints (free) and high-volume container registry/logging traffic to Interface Endpoints, you decouple your cloud billing from your data transfer volume.

Instead of your AWS bill scaling linearly with your analytics processing or EKS deployments, your NAT Gateway fees drop to a predictable baseline.

The Gateway Endpoints cost nothing, so they pay for themselves on the first byte. The Interface Endpoints pay for themselves once you cross the break-even volume above.


Not sure which side of the break-even your traffic is on? That is a five-minute question on the free review.

Frequently Asked Questions

Let's make your
infrastructure work better.

Lower cloud costs, more reliable deployments,
and systems your team can operate with confidence.