The $0.045/GB Mistake Hiding in Almost Every AWS VPC
A NAT Gateway will happily bill you to reach a free AWS service
Here's an AWS billing fact that catches almost everyone eventually: a NAT Gateway charges $0.045 per GB processed, in both directions, for everything that flows through it — including traffic to other AWS services in the same region, like S3 and DynamoDB.
Those two services also offer a free way to reach them from inside a VPC: a Gateway VPC Endpoint. It costs nothing to create, nothing per hour, and nothing per GB. It just needs to exist and be attached to the right route tables.
If it doesn't exist, none of that traffic disappears — it just quietly reroutes through the NAT Gateway instead, at $0.045/GB, forever, until someone notices. This is well-known enough in the AWS community to have a nickname: the $1,000 AWS mistake. It's rarely a single mistake, though. It's usually a missing checkbox from the original VPC setup, silently compounding every month since.
AWS's own VPC pricing page spells out the fix: "To avoid the NAT Gateway Data Processing charge… you could set up a gateway Type VPC endpoint… There are no data processing or hourly charges for using Gateway Type VPC endpoints." The footgun is documented. It just isn't defaulted.
The math
A NAT Gateway in us-east-1 bills on two meters:
- $0.045/hour (~$32.85/month) just for existing
- $0.045/GB processed — in both directions
The processing charge is what turns this into a tax. Same-region traffic between your VPC and S3 carries no data transfer charge — so when S3 traffic goes through the NAT Gateway, that $0.045/GB isn't buying you anything. It's 100% avoidable spend. At 1 TB/month it's $45; at 10 TB, $450 — real money, but spread thin enough across a bill that it rarely triggers an investigation.
Rates vary by region (sa-east-1 runs 2× higher), and if a zonal NAT Gateway in
one AZ is serving instances in another, cross-AZ data transfer ($0.01/GB each way)
stacks on top. The waste compounds quietly.
Why this specific gap is so persistent
A few things make this one unusually sticky compared to other cost issues:
- Nothing breaks. Requests to S3 and DynamoDB succeed exactly the same way whether they go through a Gateway Endpoint or a NAT Gateway. There's no error, no latency cliff, no CloudWatch alarm — just a bigger number on next month's bill.
- It's invisible in the AWS Cost Explorer service breakdown. The charge shows up under "EC2-Other" / NAT Gateway data processing, not attributed to S3 or DynamoDB. Unless you already know to look for it, there's no line item that says "you're paying NAT fees for S3 traffic."
- It's per-VPC, and most accounts have more than one VPC. A team that fixed this once, in one VPC, two years ago, can still be bleeding money in every VPC created since — a new environment, a new microservice's dedicated VPC, an acquired team's infrastructure that was never audited.
- It scales with your success. The more traffic your application sends to S3 or DynamoDB, the bigger this line item gets — completely disconnected from whether your infrastructure is efficient. A well-architected, right-sized, "no waste anywhere else" account can still be paying this tax at scale.
- Your IaC tool quietly decides for you. Terraform's
aws_vpccreates no endpoints. Neither does a bare CloudFormationAWS::EC2::VPCor the console's "VPC and more" wizard. CDK'sVpcconstruct does add an S3 Gateway Endpoint by default — but not DynamoDB — which is how accounts end up half-covered. - The endpoint exists but the routes don't. A Gateway Endpoint only helps the route tables it's associated with. Route tables added later — new subnets, new environments — silently keep using the NAT path.
"We don't use S3 that much" — probably wrong
The reason this survives audits is that S3 traffic hides behind other services. In a private subnet with no S3 endpoint, all of this crosses the NAT Gateway:
- ECR image pulls — image layers are stored in S3. Every ECS task launch and every EKS pod start downloads layers through NAT. A busy cluster re-pulling multi-GB images is often the single largest source.
- Logs: CloudTrail trails, ALB/NLB access logs, VPC Flow Logs → all land in S3.
- Backups and exports — AWS Backup vaults, RDS snapshot exports, application backups.
- Analytics — Athena reads/writes, EMR and Glue jobs, Spark shuffle to S3.
- CI/CD plumbing — CodeBuild/CodePipeline artifacts, Terraform state,
aws s3 syncin pipelines.
A team can honestly believe their S3 usage is "a few buckets" while the account moves terabytes a month through the NAT Gateway.
What it looks like when a scanner catches it
GreenOps Scan's vpc-endpoints module checks every VPC that has a NAT Gateway for
missing S3/DynamoDB Gateway Endpoints, and pulls the NAT Gateway's actual
BytesOutToDestination / BytesInFromDestination CloudWatch metrics over the last 30
days to estimate what's realistically at stake:
- severity: HIGH
module: VPC-ENDPOINTS
resource: vpc-0a1b2c3d4e5f6a7b8
resourceType: VPC
issue: NAT Gateway present with no S3 Gateway Endpoint — S3 traffic pays NAT data processing fees ($0.045/GB), ~412.6 GB processed by NAT in last 30 days
costSavings: $18.57/mo
confidence: LOW
co2Savings: 0.00 kg/mo
recommendation: Add a free VPC Gateway Endpoint for S3 so traffic routes directly instead of through the NAT Gateway. This is the exact mistake behind the well-known "$1,000 AWS bill" (missing S3 Gateway Endpoint routes S3 traffic through NAT).
That's an illustrative shape, not a specific account's real numbers — the actual
dollar figure depends entirely on how much of a VPC's NAT-processed traffic is
S3/DynamoDB-bound versus genuinely external, which is why the confidence is reported
as low: it's an upper bound estimate from total NAT throughput, not a guarantee. A
busy production VPC pushing serious S3 traffic — log ingestion, backups, static asset
uploads, data pipeline output — can post a number an order of magnitude higher than
that.
Check your account in three commands
Which VPCs have NAT Gateways:
aws ec2 describe-nat-gateways \
--filter Name=state,Values=available \
--query 'NatGateways[].{Vpc:VpcId,NatGw:NatGatewayId,Subnets:SubnetId}' \
--output table
Which of those VPCs have Gateway endpoints (S3/DynamoDB), and which route tables they cover:
aws ec2 describe-vpc-endpoints \
--filters Name=vpc-endpoint-type,Values=Gateway \
--query 'VpcEndpoints[].{Vpc:VpcId,Service:ServiceName,RouteTables:RouteTableIds}' \
--output table
Cross-reference the two lists, then check each endpoint's RouteTables against your
private route tables — an endpoint that exists but isn't associated with the right
route table is a false sense of security.
How much data the NAT actually processed (30 days):
aws cloudwatch get-metric-statistics \
--namespace AWS/NATGateway --metric-name BytesOutToDestination \
--dimensions Name=NatGatewayId,Value=nat-0123456789abcdef0 \
--start-time $(date -v-30d +%FT%TZ) --end-time $(date +%FT%TZ) \
--period 86400 --statistics Sum
(On Linux, use date -d '30 days ago' +%FT%TZ instead of -v-30d.)
Multiply GB by $0.045 for the upper bound — CloudWatch can't tell you which destination the bytes were bound for, so not all of it is avoidable. But if the number is big and there's no S3 endpoint, the overlap is usually generous.
The endpoints that aren't free: the ~200 GB rule
Gateway endpoints only exist for S3 and DynamoDB. Everything else — ECR's API, SQS, SNS, Secrets Manager, STS, KMS, CloudWatch — needs an interface endpoint, which isn't free: roughly $0.01/GB processed plus ~$7/month per AZ per endpoint.
That still beats $0.045/GB, with a clean break-even: an interface endpoint pays for itself at roughly 200 GB/month per endpoint per AZ. For high-volume chatty services — CloudWatch Logs and the ECR API are the usual suspects — that's often crossed. For everything else, the NAT path may legitimately be cheaper. The point isn't "endpoint everything"; it's that this is a calculable decision that almost nobody calculates.
The fix costs nothing and breaks nothing
This is one of the rare cost findings with essentially zero downside to fixing:
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0a1b2c3d4e5f6a7b8 \
--service-name com.amazonaws.eu-central-1.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-0123456789abcdef0
- It's additive — creating a Gateway Endpoint doesn't remove or reconfigure your NAT Gateway. Nothing else changes.
- It's free — no hourly charge, no per-GB charge, ever, for Gateway Endpoints specifically (this is different from Interface Endpoints, which do have an hourly cost — Gateway Endpoints for S3 and DynamoDB are the free kind).
- It's low-risk — the only thing worth reviewing afterward is the endpoint's default access policy, if your account needs tighter control than "anything in this VPC can reach any bucket/table it has IAM permission for."
There's no migration, no downtime, no application change. You add the endpoint, attach it to the route tables that matter, and the next request just takes the cheaper path.
Why this is worth checking even if you're sure you already fixed it
If your team has been on AWS for a while, there's a reasonable chance someone already added the S3 Gateway Endpoint to your main production VPC, years ago, after reading a similar post. That doesn't mean the account is covered:
- Every new VPC — a fresh environment, a new region, a per-team or per-service VPC in a more segmented account — starts from zero and needs the endpoint added again.
- DynamoDB is easy to miss even when S3 is covered, since it's a separate Gateway Endpoint service name, not bundled with the S3 one.
- Accounts that grew through acquisitions, terraform-module copy/paste, or infrastructure inherited from a team that's moved on are the most likely to have silent gaps, because there's no single person who remembers whether it was ever checked.
The check itself takes seconds per VPC: does a NAT Gateway exist, and if so, do Gateway VPC Endpoints for S3 and DynamoDB exist and route through the same route tables. GreenOps Scan runs this automatically as part of a normal read-only scan across every VPC in the account:
npx greenops-scan --modules vpc-endpoints
If nothing shows up, that's not a wasted five minutes — that's confirmation your account isn't paying a tax that a large fraction of AWS accounts still are, without anyone noticing.
Tags: #AWS #CloudCostOptimization #FinOps #VPC #NATGateway #NetworkCosts #GreenOps
