All articles
DevOps & CloudJuly 19, 2026 6 min read

AWS NAT Gateway Bills Ate Our Margins. Here's How We Cut Them 78%.

A single misconfigured VPC route turned our NAT Gateway into a five-figure monthly line item. Here's the audit trail, the fixes, and what we'd do differently.

AWS NAT Gateway Bills Ate Our Margins. Here's How We Cut Them 78%.

Our AWS bill for a mid-sized staging environment quietly grew by four figures a month for two quarters before anyone flagged it. The culprit wasn't compute or storage — it was a NAT Gateway silently metering every S3 GET, every ECR pull, every CloudWatch Logs PUT at $0.045 per GB. This is the audit we ran, the fixes that stuck, and the ones that weren't worth it.

Why NAT Gateway bills sneak up on you

NAT Gateway pricing has two axes: an hourly charge per gateway (roughly $0.045/hour in most regions) and a per-GB data processing charge (also around $0.045/GB, on top of any egress to the internet). The hourly cost is predictable. The data processing cost is where teams get burned, because it applies to traffic going to AWS services too — unless you route around it.

If your private subnet ECS tasks pull container images from ECR, ship logs to CloudWatch, read objects from S3, or talk to DynamoDB via the public endpoint, every one of those bytes goes through NAT. You're paying AWS to send data to AWS.

In our case, three workloads were responsible for the bulk of the spend:

  • A batch pipeline pulling ~800 GB/day of raw events from S3 into a private-subnet EKS cluster
  • A CI runner fleet in a private subnet pulling multi-GB container images from ECR on every job
  • A logging sidecar shipping verbose JSON to CloudWatch Logs

None of these needed the public internet. All of them were routing through NAT because that was the default when the VPC was built in 2022 and nobody revisited it.

Step 1: Find out what's actually flowing through NAT

Before you touch routing, you need data. VPC Flow Logs are the honest answer here, but the volume can be brutal. We enabled flow logs on the NAT Gateway's ENI only, wrote them to S3 in Parquet, and queried with Athena.

A minimal query to find your top talkers by destination:

SELECT
  dstaddr,
  SUM(bytes) / 1024 / 1024 / 1024 AS gb_out,
  COUNT(*) AS flows
FROM vpc_flow_logs
WHERE action = 'ACCEPT'
  AND start_time BETWEEN
    from_iso8601_timestamp('2026-01-01')
    AND from_iso8601_timestamp('2026-01-08')
GROUP BY dstaddr
ORDER BY gb_out DESC
LIMIT 50;

Cross-reference the top destination IPs against AWS's published IP ranges (the ip-ranges.json file). If the top offenders resolve to S3, DynamoDB, ECR, or CloudWatch prefixes, you have a routing problem, not a workload problem.

What we found

Over seven days, roughly 71% of NAT-processed bytes went to S3 prefixes in the same region. Another 14% went to ECR and CloudWatch. Only about 15% was actual internet-bound traffic — third-party APIs, package registries, webhook deliveries. We were paying NAT rates for AWS-internal traffic that could have been free or nearly so.

Step 2: Gateway endpoints are the cheapest win

S3 and DynamoDB support gateway endpoints. They cost nothing per hour, nothing per GB, and they work by injecting a prefix list into your route table. Traffic to S3 in the same region stops going through NAT entirely.

Here's the Terraform we used to add S3 and DynamoDB gateway endpoints across both private route tables:

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

  policy = data.aws_iam_policy_document.s3_endpoint.json
}

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
}

A note on the endpoint policy: the default is */*. In our experience, tightening it to specific buckets is worth the extra ten minutes. If a compromised task can suddenly reach every S3 bucket in the region via a free, high-bandwidth path, you've traded one problem for another.

After deploying the S3 gateway endpoint, our NAT data processing dropped by roughly 68% within 24 hours. This was the single biggest lever.

Step 3: Interface endpoints for the rest

ECR, CloudWatch Logs, Secrets Manager, SSM, and STS use interface endpoints (PrivateLink). These aren't free — they cost around $0.01/hour per endpoint per AZ, plus $0.01/GB processed. But compared to NAT's $0.045/GB, they pay for themselves quickly if you're moving nontrivial data.

The math we ran, per AZ, per endpoint:

  • Break-even on hourly cost alone: ~$7.20/month per endpoint per AZ
  • Break-even on data: any workload pushing more than ~10 GB/month through that service

For ECR specifically, you need two endpoints: ecr.api and ecr.dkr. And if your images live in S3-backed layers (they do), the S3 gateway endpoint has to already be in place or you'll still pay NAT rates for the actual layer downloads. This tripped us up on the first pass — we added ECR endpoints, saw only a modest drop, and had to go back and check flow logs to realise the layer bytes were still going through NAT.

Which interface endpoints were worth it for us

  • ECR (api + dkr): yes, easily. CI runners pulled hundreds of GB/day of images.
  • CloudWatch Logs: yes. The logging sidecar was chattier than we thought.
  • Secrets Manager: marginal. Low data volume, but we added it for the latency and blast-radius benefit, not the cost.
  • STS: no. Traffic was negligible.
  • SSM: yes, but only because we use Session Manager heavily.

We stopped short of endpointing every service. There's a real operational cost to managing dozens of interface endpoints, and the per-AZ hourly charges add up if you're not using them.

Step 4: The workload-level fixes we almost skipped

Endpoints handle the routing problem. They don't handle the fact that some workloads are just noisy.

The logging sidecar was shipping full request bodies to CloudWatch. We dropped log level to INFO in staging, added structured field filtering, and reduced log volume by about 60% on its own. That's 60% less data through the (now cheaper) CloudWatch endpoint, but it also reduced our Logs ingestion bill, which is a separate line item at ~$0.50/GB.

CI runners were pulling base images fresh on every job. We added a warm-cache layer on the runner hosts and pinned base image digests, cutting ECR pulls by roughly half.

These aren't NAT fixes. They're workload hygiene. But they compound with the endpoint work.

Step 5: Kill the redundant NAT Gateways

One pattern we've seen repeatedly: teams deploy one NAT Gateway per AZ for high availability, even in non-production environments where a 30-minute AZ outage is survivable. That's three gateways at ~$32/month each in hourly fees before a single byte flows.

For staging and dev, we consolidated to a single NAT Gateway. If the AZ hosting it goes down, staging goes down. We're fine with that. Production kept its multi-AZ setup — that's a reliability decision, not a cost one.

What the numbers looked like at the end

Across the environment we audited, NAT data processing charges dropped by roughly 78% over the following month. The hourly NAT charges dropped by about a third from the staging consolidation. Net monthly savings covered the interface endpoint costs several times over and paid for the engineering time within the first billing cycle.

Production saw a smaller percentage drop (closer to 55%) because a larger share of its traffic genuinely goes to the internet — payment processors, third-party APIs, outbound webhooks. That's the ceiling: NAT is doing real work for real internet traffic, and there's no endpoint that saves you from that.

Where we'd start

If you haven't looked at your NAT Gateway bill in six months, do this in order:

  1. Enable flow logs on the NAT Gateway ENI for a week. Query the top destinations.
  2. Add S3 and DynamoDB gateway endpoints. They're free and the routing change is atomic per route table.
  3. Look at your ECR and CloudWatch Logs volume. If either is above ~50 GB/month, add interface endpoints.
  4. In non-production, ask honestly whether you need multi-AZ NAT. Usually you don't.
  5. Only then look at workload-level noise reduction.

The first two steps take an afternoon and pay for themselves immediately. Everything after that is diminishing returns, but worth measuring. If you want a second pair of eyes on a VPC audit or a broader cloud infrastructure review, we're happy to help — but honestly, most teams can do this one themselves with flow logs and a quiet Friday.

#AWS#Networking#Cost Optimization#VPC#DevOps

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project