FINOPS CUT 80%

AWS NAT Gateway Costs — Why It's Eating 40% of Small Startup Bills

By Akshay Ghalme · April 14, 2026 · 14 min read

AWS NAT Gateway charges $0.045 per hour plus $0.045 per GB of data processed — and on small startup accounts, it routinely becomes the single largest line item on the monthly bill. The fix is almost always the same: add a free S3 Gateway Endpoint, add Interface Endpoints for ECR and a handful of other services, and consolidate to a single NAT Gateway for non-production. Most teams cut their NAT Gateway bill by 80% in an afternoon.

I have audited a lot of small-company AWS accounts. The pattern is almost always the same: the team spins up a proper three-AZ VPC, drops their workloads into private subnets like the AWS docs say to, and six months later someone looks at the bill and asks "why are we paying $800 a month for NAT Gateway on a $2,000 account?"

The answer is never one big thing. It is a hundred small things — container images pulled from ECR, application logs sent to CloudWatch, backups uploaded to S3, secrets fetched from Secrets Manager — all quietly flowing through the NAT Gateway at $0.045 per gigabyte when most of them did not need to touch the public internet at all.

This post is the complete playbook I use to audit and fix NAT Gateway costs. It covers how the pricing actually works, how to find out what is flowing through your NAT Gateway right now, and the exact VPC Endpoint patterns that eliminate most of the charges.

How NAT Gateway Pricing Actually Works

AWS charges for NAT Gateway in two separate dimensions, and the second one is what wrecks your bill.

Here is the pricing in a typical region like ap-south-1 (Mumbai) or us-east-1 (N. Virginia):

NAT Gateway hourly rate:      $0.045 per hour
NAT Gateway data processing:  $0.045 per GB
Data transfer out (to internet):  $0.09 per GB (standard AWS egress)

The hourly rate alone is already $32.40 per month per NAT Gateway. If you are following the "best practice" of one NAT Gateway per Availability Zone in a three-AZ VPC, that is $97 per month just for uptime, before any traffic touches them.

The data processing charge is where the real damage happens. Every single byte that goes through the NAT Gateway — inbound and outbound — is charged at $0.045 per GB. And unlike the regular internet egress charge, the data processing charge applies even when the destination is another AWS service in the same region. Your Lambda fetching an S3 object over NAT? That costs you $0.045 per GB to reach a service inside the same region.

On top of that, if the destination is actually the public internet, you also pay the standard $0.09 per GB egress charge on top. So traffic going out to the internet through a NAT Gateway costs you $0.135 per GB in total — one and a half times what you would pay from a public subnet.

Here is what a typical "default" VPC setup looks like — every AWS service call from your private workloads gets funneled through the NAT Gateway at $0.045/GB, even when the destination is another AWS service inside the same region:

graph LR APP[App / ECS / Lambda
in Private Subnet] -->|$0.045/GB| NAT[NAT Gateway] NAT -->|$0.045/GB| S3[(S3)] NAT -->|$0.045/GB| ECR[ECR
container images] NAT -->|$0.045/GB| LOGS[CloudWatch Logs] NAT -->|$0.045/GB| SM[Secrets Manager] NAT -->|$0.135/GB| INET[Public Internet] classDef bad fill:#4A1DB5,stroke:#ff6b6b,stroke-width:3px,color:#fff; classDef svc fill:#6C3CE1,stroke:#00D4AA,stroke-width:2px,color:#fff; class NAT bad; class APP,S3,ECR,LOGS,SM,INET svc;

Why This Adds Up So Fast on Small Accounts

On a mature enterprise account with reserved capacity and committed spend, NAT Gateway is usually 5-10% of the bill. It is an annoyance but not a crisis. On a small startup or indie account, the same NAT Gateway often lands at 30-45% of the total bill.

The math is brutal. On a small account, the compute layer is cheap — maybe a couple of t3.medium instances at $30 each, a small RDS at $25, some Lambda and S3 usage. The baseline might be $150 to $300 a month. But every time a container restarts and pulls a 500 MB image from ECR through NAT, that single pull costs you $0.023. A CI pipeline that runs 50 times a day becomes a $35 monthly line item just for image pulls. A production workload that logs 2 GB per day to CloudWatch Logs through NAT is $2.70 a month per environment.

None of these are individually expensive. But they stack up on a small baseline, and the NAT Gateway is the silent intermediary taking a cut on all of it.

Real Example From an Audit I Did

Here is a real cost breakdown I pulled from a startup account with roughly $2,100 in monthly AWS spend:

EC2 instances (compute):       $420   (20%)
RDS (database):                $280   (13%)
S3 storage:                    $110   (5%)
CloudWatch:                    $190   (9%)
Data transfer:                 $140   (7%)
NAT Gateway (hourly):          $97    (5%)
NAT Gateway (data processing): $820   (39%)
Everything else:               $43    (2%)
-----------------------------------------
Total:                         $2,100

The single line item "NAT Gateway data processing" was 39% of the bill. The team had three NAT Gateways (one per AZ) and roughly 18 TB of data per month flowing through them. The bulk of it was ECR image pulls from a busy CI/CD pipeline, S3 backups from an EC2 fleet, and application logs being shipped to CloudWatch.

After the audit and fixes I walk through below, the NAT Gateway data processing line dropped to $135 a month. The total bill went from $2,100 to $1,350. Same workloads, same availability, fewer middlemen.

Step 1: Audit What Is Actually Flowing Through Your NAT Gateway

Before changing anything, you need to know what is going through the NAT Gateway and where it is going. You cannot optimize traffic you cannot see.

AWS gives you two tools for this: Cost Explorer for the cost side and VPC Flow Logs for the traffic side. You need both.

Check Cost Explorer First

Go to Cost Explorer and set these filters:

  • Service: EC2-Other
  • Usage Type Group: EC2: NAT Gateway
  • Group by: Usage type
  • Time range: last 3 months, daily granularity

You will see three usage types:

NatGateway-Hours          (hourly uptime charge)
NatGateway-Bytes          (data processing, $0.045/GB)
NatGateway-DataTransfer   (internet egress on top)

If NatGateway-Bytes is more than 20% of your total bill, you have a problem worth fixing. If it is more than 40%, drop everything and fix it this week.

Enable VPC Flow Logs

Cost Explorer tells you how much is flowing. VPC Flow Logs tell you what is flowing. Enable Flow Logs on the private subnets (or the VPC itself) and send them to an S3 bucket or CloudWatch Logs.

resource "aws_flow_log" "vpc" {
  log_destination      = aws_s3_bucket.flow_logs.arn
  log_destination_type = "s3"
  traffic_type         = "ALL"
  vpc_id               = aws_vpc.main.id
}

Let it run for 24 to 48 hours so you get a representative sample. Then query it with Athena to find the top talkers:

SELECT
  srcaddr,
  dstaddr,
  SUM(bytes) / 1024 / 1024 / 1024 AS gb_transferred
FROM vpc_flow_logs
WHERE dstaddr NOT LIKE '10.%'
  AND dstaddr NOT LIKE '172.%'
  AND dstaddr NOT LIKE '192.168.%'
  AND day >= '2026/04/01'
GROUP BY srcaddr, dstaddr
ORDER BY gb_transferred DESC
LIMIT 50;

This gives you a ranked list of source IP to destination IP pairs with the byte volume. The destination IPs that belong to AWS services (S3, ECR, DynamoDB, CloudWatch, Secrets Manager) are your fix targets. Every gigabyte going to those services is a gigabyte you can eliminate from your NAT Gateway bill.

Tip: AWS publishes its IP ranges at https://ip-ranges.amazonaws.com/ip-ranges.json. You can join your top destination IPs against this list to identify which AWS service each one belongs to.

Step 2: Add the Free S3 Gateway Endpoint

This is the single biggest win and it costs nothing. Zero. No hourly charge, no per-GB charge.

A VPC Gateway Endpoint is a special route that AWS adds to your route table so that any traffic destined for S3 or DynamoDB goes directly to those services over the AWS private network instead of through the NAT Gateway. It is completely free. The only two services that support Gateway Endpoints are S3 and DynamoDB, but those two services alone usually account for a huge chunk of NAT traffic on most accounts.

Adding it in Terraform is three lines:

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
}

That is it. No Security Groups, no subnets, no ENIs. AWS adds a prefix list to every route table you listed, and from that moment onward, traffic to S3 and DynamoDB from private subnets bypasses the NAT Gateway entirely.

On the $2,100 account I mentioned above, adding the S3 Gateway Endpoint alone cut the NAT data processing charge from $820 to about $410. Half the bill, gone, from 3 lines of Terraform.

Here is what the architecture looks like after adding the Gateway Endpoint plus Interface Endpoints (covered in the next step). Only traffic to the actual public internet still goes through the NAT Gateway — everything else bypasses it:

graph LR APP[App / ECS / Lambda
in Private Subnet] -->|FREE| S3GW[S3 Gateway Endpoint] S3GW --> S3[(S3)] APP -->|FREE| DDBGW[DynamoDB Gateway Endpoint] DDBGW --> DDB[(DynamoDB)] APP -->|$0.01/GB| IE[Interface Endpoints
PrivateLink] IE --> ECR[ECR] IE --> LOGS[CloudWatch Logs] IE --> SM[Secrets Manager] APP -->|only external traffic| NAT[NAT Gateway] NAT --> INET[Public Internet
3rd party APIs only] classDef good fill:#047857,stroke:#00D4AA,stroke-width:3px,color:#ffffff; classDef ok fill:#6C3CE1,stroke:#00D4AA,stroke-width:2px,color:#ffffff; classDef svc fill:#4A1DB5,stroke:#00D4AA,stroke-width:2px,color:#ffffff; class S3GW,DDBGW good; class IE,NAT ok; class APP,S3,DDB,ECR,LOGS,SM,INET svc;

Step 3: Add Interface Endpoints for ECR, CloudWatch, and Secrets Manager

For every other AWS service, you need an Interface Endpoint (also called a PrivateLink endpoint). These are not free, but they are much cheaper than NAT Gateway for high-volume traffic.

Interface Endpoint pricing:

Hourly charge:     $0.01 per hour per AZ ($7.30/month per AZ)
Data processing:   $0.01 per GB

Compare that to NAT Gateway at $0.045 per GB. An Interface Endpoint pays for itself as soon as you push roughly 730 GB per month through it (the hourly cost divided by the per-GB savings). If you have a chatty service sending multi-terabyte traffic, Interface Endpoints are a no-brainer.

The services you almost always want Interface Endpoints for:

  • ECR (ecr.api and ecr.dkr): Container image pulls. Usually the single biggest offender on ECS/EKS accounts.
  • CloudWatch Logs (logs): Application log shipping. High-volume on any real workload.
  • CloudWatch Monitoring (monitoring): Custom metric pushes.
  • Secrets Manager (secretsmanager): Every secret fetch on application startup.
  • SSM (ssm, ssmmessages, ec2messages): Required for Session Manager and Parameter Store.
  • STS (sts): Token refresh for IAM roles.
  • KMS (kms): Encryption key operations.

In Terraform, create them using for_each so it stays tidy:

locals {
  interface_endpoints = [
    "ecr.api",
    "ecr.dkr",
    "logs",
    "monitoring",
    "secretsmanager",
    "ssm",
    "ssmmessages",
    "ec2messages",
    "sts",
    "kms",
  ]
}

resource "aws_vpc_endpoint" "interface" {
  for_each = toset(local.interface_endpoints)

  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.vpc_endpoints.id]
  private_dns_enabled = true
}

resource "aws_security_group" "vpc_endpoints" {
  name        = "vpc-endpoints-sg"
  description = "Allow HTTPS from private subnets to VPC endpoints"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }
}

The private_dns_enabled = true setting is the important part. It makes AWS automatically resolve the standard service hostnames (like ecr.ap-south-1.amazonaws.com) to the endpoint ENI inside your VPC. You do not need to change a single line of application code — traffic that used to exit through NAT now silently routes to the endpoint instead.

Step 4: Consolidate NAT Gateways for Non-Production

Every "production-grade" VPC reference architecture tells you to deploy one NAT Gateway per Availability Zone. That is good advice for production. It is wasteful advice for dev, staging, and QA.

A single NAT Gateway in one AZ can serve private subnets in all AZs of the same VPC. You just point all private route tables at the same NAT Gateway. The only downside is that if the AZ hosting the NAT Gateway fails, private subnets in other AZs lose outbound internet connectivity until you fix it.

For non-production environments, that tradeoff is almost always worth it. An AZ failure in dev is a minor inconvenience, not a business emergency. Going from 3 NAT Gateways to 1 saves you $65 per month per environment just on the hourly charge.

In Terraform:

locals {
  nat_gateway_count = var.environment == "prod" ? length(var.azs) : 1
}

resource "aws_nat_gateway" "main" {
  count         = local.nat_gateway_count
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
}

resource "aws_route_table" "private" {
  count  = length(var.azs)
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    # Private subnets in all AZs route to NAT Gateway 0 in non-prod
    nat_gateway_id = aws_nat_gateway.main[
      local.nat_gateway_count == 1 ? 0 : count.index
    ].id
  }
}

Three dev environments, one NAT Gateway each instead of three. That is $195 a month saved before you even look at traffic optimization.

Step 5: Eliminate Cross-AZ Traffic Through NAT

Here is a subtle one that almost nobody catches. When you have one NAT Gateway per AZ, you want traffic from each private subnet to use the NAT Gateway in its own AZ. If a private subnet in ap-south-1a accidentally routes through a NAT Gateway in ap-south-1b, you pay a cross-AZ data transfer charge ($0.01 per GB each way) on top of the NAT Gateway data processing charge.

This usually happens when people get lazy with route tables and point every private subnet at the same NAT Gateway. In production that is a cost bug, not a cost saving — it is cheaper to have three NAT Gateways with AZ-local routing than one NAT Gateway receiving cross-AZ traffic from all three.

The fix is to have one route table per private subnet (or at least one per AZ) and have each one point at the NAT Gateway in the same AZ. I cover this in more detail in my guide on setting up a production VPC on AWS.

Step 6: Consider a NAT Instance for Dev

If you want to go all the way, you can replace NAT Gateway entirely with a NAT instance for non-production environments. A t4g.nano NAT instance running on Graviton costs about $3 per month and can comfortably handle the traffic of a small dev environment.

The tradeoffs:

  • No data processing charges — you just pay for standard EC2 data transfer out, not the extra NAT Gateway $0.045/GB.
  • You manage it yourself — patches, failure recovery, restarts. This is real operational work.
  • Lower bandwidth ceiling — a t4g.nano maxes out around 5 Gbps and has burst credits. Fine for dev, not fine for production.
  • Single point of failure — if the instance crashes, private subnets lose outbound connectivity.

For dev and staging where cost matters more than uptime, NAT instance is a legitimate choice. The community project fck-nat provides a ready-to-use NAT instance AMI that I have seen work well for small teams. For production, stick with NAT Gateway.

The Full Optimization Checklist

Here is the playbook I run on every NAT Gateway audit, in order:

  1. Check Cost Explorer for NatGateway-Bytes as a percentage of total bill.
  2. Enable VPC Flow Logs on private subnets and query the top talkers after 24-48 hours.
  3. Add the S3 Gateway Endpoint (free, always do this).
  4. Add the DynamoDB Gateway Endpoint if you use DynamoDB (also free).
  5. Add Interface Endpoints for ECR, CloudWatch Logs, Secrets Manager, SSM, STS, and KMS.
  6. Consolidate to one NAT Gateway per non-production environment.
  7. Verify AZ-local routing in production to avoid cross-AZ charges.
  8. Re-run the audit 7 days later and confirm the bill dropped.

Do these six things and your NAT Gateway bill usually drops by 70-85%. The remaining traffic is typically unavoidable — calls to third-party APIs, package downloads from PyPI or npm, webhooks to external services — and there is no free lunch for those.

Things That Look Like They Should Help But Do Not

A few optimizations that sound smart but are not worth the effort:

  • Moving workloads to public subnets to avoid NAT entirely. This saves NAT costs but loses the security posture of private subnets. Not worth it unless your workload genuinely needs to be public-facing.
  • Using a proxy server in a public subnet instead of NAT Gateway. You save the $0.045/GB data processing charge but add operational overhead and a new failure point. For a small account, the Interface Endpoint route is cleaner.
  • Compressing application logs before sending to CloudWatch. CloudWatch already compresses logs on the wire. The client-side compression buys you almost nothing.
  • Switching to an S3 VPC Endpoint Policy to "block external traffic". Endpoint policies are for security, not cost. They do not change how traffic is routed.

Frequently Asked Questions

How much does an AWS NAT Gateway really cost?

A NAT Gateway costs $0.045 per hour plus $0.045 per GB of data processed. That is about $32 per month just for uptime, before any traffic. With a typical 500 GB per month of outbound traffic from private subnets, you are looking at roughly $55 per NAT Gateway per month. Multiply by the number of Availability Zones you deploy in and it adds up fast.

Why is NAT Gateway so expensive on small AWS accounts?

Because it charges for every byte flowing through it, including traffic to other AWS services in the same region. A container pulling images from ECR, a Lambda fetching objects from S3, or a pod hitting DynamoDB all go through the NAT Gateway by default if they run in private subnets. Teams end up paying $0.045 per GB to reach services that could be accessed for free using VPC Gateway Endpoints.

What is a VPC Gateway Endpoint and is it free?

A VPC Gateway Endpoint lets resources in your VPC reach S3 and DynamoDB without going through a NAT Gateway or the public internet. It is completely free — no hourly charge, no data processing charge. The only services that support Gateway Endpoints are S3 and DynamoDB. For every other AWS service, you need an Interface Endpoint, which is cheaper than NAT but not free.

Can I run a single NAT Gateway instead of one per Availability Zone?

Yes, you can. A single NAT Gateway in one AZ will serve traffic from private subnets in all AZs of your VPC. This cuts your hourly NAT Gateway cost by two thirds in a three-AZ setup. The tradeoff is availability — if that AZ fails, private subnets in the other AZs lose outbound internet access. For dev and staging, the savings are worth it. For production, use one NAT Gateway per AZ.

How do I find out which workloads are sending traffic through my NAT Gateway?

Enable VPC Flow Logs on your private subnets and query them with Athena. Look for destination addresses outside your VPC CIDR and group by source IP, destination IP, and bytes transferred. The top talkers are almost always containers pulling images from ECR, applications fetching from S3, or monitoring agents sending data to CloudWatch. Each of those has a VPC Endpoint alternative.

Do Interface Endpoints work across VPC peering?

No. Interface Endpoints are only reachable from within the VPC they are deployed in, not across VPC peering connections by default. If you have a hub-and-spoke architecture, you either deploy endpoints in each spoke VPC or use Transit Gateway with more advanced routing. For most small accounts, endpoints in each VPC is the simpler answer.


Keep Going — More AWS Cost Optimization Guides

NAT Gateway is usually the biggest hidden cost on small AWS accounts, but it is rarely the only one. Two guides that pair well with this one:

If you want the VPC Endpoint setup from this guide as a drop-in Terraform module, I am packaging it into the terraform-modules library — drop your email on the newsletter page to get notified when it ships.

AG

Akshay Ghalme

AWS DevOps Engineer with 3+ years building production cloud infrastructure. AWS Certified Solutions Architect. Currently managing a multi-tenant SaaS platform serving 1000+ customers.

More Guides & Terraform Modules

Every guide comes with a matching open-source Terraform module you can deploy right away.