How to Reduce AWS NAT Gateway Data Processing Charges (Without a NAT Instance)

NAT Gateway is one of the quietest line items in an AWS bill. The hourly charge is fine — about $32/month per gateway. The killer is the $0.045/GB data processing fee on top of standard egress.

Reduce AWS NAT Gateway Data Charges

NAT Gateway is one of the quietest line items in an AWS bill. The hourly charge is fine — about $32/month per gateway. The killer is the $0.045/GB data processing fee on top of standard egress, which compounds the moment your private subnets start pulling container images, reading from S3, or talking to any AWS service over the public endpoint.

On one of the EKS clusters I help run, NAT data processing was sitting at $640/month before I touched it. Here's the actual sequence I went through to bring it under $90, no NAT instances involved.

1. Find out what's actually going through NAT

Cost Explorer hides NAT under EC2-Other. To see it, group by Usage Type and filter on NatGateway-Bytes. That tells you the spend, not the source.

For the source, enable VPC Flow Logs to S3 (Parquet, hourly partitioning) and run:

SELECT srcaddr,
       SUM(bytes) / 1024.0 / 1024.0 / 1024.0 AS gb
FROM vpc_flow_logs
WHERE year = '2026' AND month = '05'
  AND dstaddr NOT LIKE '10.%'
  AND dstaddr NOT LIKE '172.%'
  AND dstaddr NOT LIKE '192.168.%'
  AND action = 'ACCEPT'
GROUP BY srcaddr
ORDER BY gb DESC
LIMIT 25;

This gives you the pod / node IPs sending the most bytes out. Reverse-lookup the destinations on the top offenders and you'll almost always find three culprits: ECR, S3, and DynamoDB.

2. Add the free gateway endpoints first

S3 and DynamoDB both have gateway endpoints, which cost nothing — no hourly, no data processing. There is no reason not to have them.

Terraform:

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = aws_route_table.private[*].id
}

resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${var.region}.dynamodb"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = aws_route_table.private[*].id
}

On my cluster this removed ~1.2 TB/month of DynamoDB traffic and a chunk of S3 reads immediately.

3. Add interface endpoints for ECR (the big one)

ECR is the silent NAT-killer on Kubernetes. Every pod cold-start that pulls a 400 MB image goes out NAT and back in unless you have endpoints. You need both:

locals {
  ecr_services = ["ecr.api", "ecr.dkr"]
}

resource "aws_vpc_endpoint" "ecr" {
  for_each            = toset(local.ecr_services)
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${var.region}.${each.key}"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true
}

Gotcha: ECR stores the actual image layers in S3, so the ECR endpoints alone don't fix layer pulls. You must also have the S3 gateway endpoint from step 2, otherwise the manifest comes through the endpoint and the layers still go through NAT.

Cost of interface endpoints: ~$7.30/month per AZ per service. For a 3-AZ cluster with 2 ECR services that's ~$44/month — trivial compared to what ECR-via-NAT was costing.

4. Things that sound clever but aren't worth it

  • Single NAT across AZs. You save one NAT's hourly fee (~$32) but pay $0.01/GB cross-AZ for every byte your other-AZ workloads send to it. Break-even is around 3.2 TB/month of cross-AZ NAT traffic, and you've added a SPOF.
  • NAT instance. Saves maybe $25-30/month per AZ vs Gateway, but you own patching, HA, and throughput limits. Not worth it above toy scale.
  • PrivateLink to third-party SaaS. Worth it only if the provider charges you NAT-comparable rates on their side. Usually they don't.

5. Measurement

Give it 48 hours after rolling out endpoints, then re-run the Athena query. On my cluster the NatGateway-Bytes usage type dropped from ~14 TB to ~1.8 TB/month. Spend went from $640 to ~$85 (hourly + residual processing).

I'm keeping a running set of single-lever cost playbooks with full Terraform and before/after CloudWatch graphs at cloudcostcutter.cloud — the NAT one has the full Athena schema and a per-workload breakdown if you want the long version.

Rachel Goldberg
About the Author Rachel Goldberg

Multi-cloud strategist comparing AWS, GCP, and Azure cost levers across real-world workloads.