# NAT Gateway is the most expensive line you don't see

It isn't on the bill. Not as "NAT Gateway", anyway. It's under *EC2-Other*, a usage type called `NatGateway-Hours` next to another called `NatGateway-Bytes`, and in Cost Explorer's default view it's folded into the EC2 line, which is why a team that runs no EC2 instances can look at a $100 EC2 charge and assume it's a mistake.

It's not a mistake. It's $0.045 an hour, every hour, for each NAT gateway you have, before a single byte goes through it, plus $0.045 for every gigabyte that does. On [the three-account bill we published](/en/blog/aws-bill-of-a-three-person-startup) it was 13 % of the total, more than Lambda, DynamoDB and WAF combined, for a component that does nothing except let private things reach the internet. This article is why you have it, and how we went from three to one.

## Why you have a NAT gateway

Because something is in a private subnet. Private subnets have no route to the internet gateway, on purpose: nothing on the internet can reach them. The price is that they can't reach the internet either, and "the internet" includes the AWS API endpoints, so a Lambda in a private subnet can't call S3, Secrets Manager or SQS without a way out. The way out is a NAT gateway in a public subnet, with a route from the private subnets to it.

Our reason was Aurora. The database is in a private subnet, correctly. The Lambdas that talk to it have to be in the VPC to reach it. Once they're in the VPC, they need the NAT for everything else they do: fetching a secret, putting a message on a queue, calling a third-party API. One requirement, database access, pulled a dozen functions into the VPC and a NAT into every account.

<div class="article-figure">
<svg viewBox="0 0 900 260" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagram of a VPC. A Lambda in a private subnet needs Aurora, which is also private, and that works directly. The same Lambda calling S3, Secrets Manager or a third-party API is routed through a NAT gateway in a public subnet, to the internet gateway, and back into AWS, paying 4.5 cents per gigabyte. Two alternative paths are shown: a free gateway endpoint for S3 and DynamoDB inside the VPC, and an interface endpoint for Secrets Manager at 7 dollars a month.">
<defs><marker id="arrN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#9aa3c7"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="20" y="20" width="560" height="220" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/><text x="40" y="44" fill="#9aa3c7" font-size="11">VPC</text>
<rect x="40" y="60" width="240" height="160" rx="10" fill="#0d1120" stroke="#2a3150"/><text x="60" y="80" fill="#9aa3c7" font-size="11">private subnet</text>
<rect x="60" y="96" width="90" height="44" rx="8" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="105" y="122" text-anchor="middle" fill="#f1f3ff">Lambda</text>
<rect x="60" y="160" width="90" height="44" rx="8" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="105" y="186" text-anchor="middle" fill="#f1f3ff">Aurora</text>
<line x1="105" y1="142" x2="105" y2="158" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrN)"/>
<rect x="170" y="96" width="100" height="44" rx="8" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5" stroke-dasharray="4,3"/><text x="220" y="114" text-anchor="middle" fill="#4fffb0" font-size="11">gateway endpoint</text><text x="220" y="130" text-anchor="middle" fill="#9aa3c7" font-size="10">S3 · DynamoDB · free</text>
<rect x="170" y="160" width="100" height="44" rx="8" fill="#151b2e" stroke="#ffd166" stroke-width="1.5" stroke-dasharray="4,3"/><text x="220" y="178" text-anchor="middle" fill="#ffd166" font-size="11">interface endpoint</text><text x="220" y="194" text-anchor="middle" fill="#9aa3c7" font-size="10">Secrets Manager · $7/mo</text>
<line x1="152" y1="112" x2="168" y2="112" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrN)"/>
<line x1="152" y1="126" x2="168" y2="176" stroke="#ffd166" stroke-width="1.5" marker-end="url(#arrN)"/>
<rect x="320" y="60" width="240" height="160" rx="10" fill="#0d1120" stroke="#2a3150"/><text x="340" y="80" fill="#9aa3c7" font-size="11">public subnet</text>
<rect x="360" y="110" width="160" height="60" rx="8" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="440" y="134" text-anchor="middle" fill="#ff6b8a" font-weight="700">NAT gateway</text><text x="440" y="154" text-anchor="middle" fill="#9aa3c7" font-size="10">$0.045 / h + $0.045 / GB</text>
<path d="M152,104 C250,50 330,60 358,130" fill="none" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrN)"/><text x="260" y="56" text-anchor="middle" fill="#ff6b8a" font-size="10">everything else, by default</text>
<line x1="522" y1="140" x2="600" y2="140" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrN)"/>
<rect x="602" y="110" width="120" height="60" rx="8" fill="#151b2e" stroke="#9aa3c7" stroke-width="1.5"/><text x="662" y="134" text-anchor="middle" fill="#f1f3ff">internet</text><text x="662" y="154" text-anchor="middle" fill="#9aa3c7" font-size="10">gateway</text>
<line x1="724" y1="140" x2="780" y2="140" stroke="#9aa3c7" stroke-width="1.5" marker-end="url(#arrN)"/>
<rect x="782" y="90" width="100" height="100" rx="8" fill="#151b2e" stroke="#9aa3c7" stroke-width="1.5"/><text x="832" y="116" text-anchor="middle" fill="#f1f3ff" font-size="11">S3 · SQS</text><text x="832" y="134" text-anchor="middle" fill="#f1f3ff" font-size="11">Secrets Mgr</text><text x="832" y="152" text-anchor="middle" fill="#f1f3ff" font-size="11">3rd-party APIs</text><text x="832" y="170" text-anchor="middle" fill="#9aa3c7" font-size="10">back into AWS</text>
</g>
</svg>
</div>

## What it actually cost us

Three accounts, one NAT each, one availability zone each (we'd deliberately not done one per AZ, which would have tripled it):

| | Hours | Bytes | Monthly |
|---|---|---|---|
| prod | $33 | ~60 GB × $0.045 = $3 | $36 |
| staging | $33 | ~5 GB | $33 |
| dev | $33 | ~10 GB | $33 |
| **Total** | | | **$102** |

Notice the shape. It's almost entirely the hourly charge. The bytes are nothing, because most of what goes through the NAT is small API calls. So the usual advice, "reduce your NAT data processing", would have saved us three dollars. The lever is the *count*.

## Five ways to need fewer of them

**1. Gateway endpoints for S3 and DynamoDB. Free.** A gateway endpoint is a route-table entry that sends traffic for S3 or DynamoDB through AWS's network instead of out and back. It costs nothing, it's two lines of CDK, and it should be in every VPC on day one regardless of NAT. It removed about half of our NAT bytes, which was worth $1.50. Do it anyway; the reason is latency and not paying egress for S3 traffic, not the NAT.

```ts
vpc.addGatewayEndpoint('S3', { service: ec2.GatewayVpcEndpointAwsService.S3 });
vpc.addGatewayEndpoint('DynamoDB', { service: ec2.GatewayVpcEndpointAwsService.DYNAMODB });
```

**2. Interface endpoints for the services you actually call. $7.30 a month each.** An interface endpoint is a network interface in your subnet with a private IP for one AWS service: Secrets Manager, SQS, ECR, CloudWatch Logs, STS. Each costs about $7.30 a month plus $0.01 a gigabyte. The arithmetic: an endpoint pays for itself only if it lets you *remove* a NAT, or if you push more than about 700 GB a month through it. Five endpoints to cover everything our Lambdas call is $37, which is more than the NAT they'd replace. So we use them in exactly one place, below.

**3. Get out of the VPC.** This is the big one. A Lambda only needs to be in the VPC if it talks to something that's only reachable there. We audited the dozen functions: seven talked to Aurora, five didn't. The five were in the VPC because the CDK construct had `vpc:` set as a default on our function factory. Removing them from the VPC took one flag and removed them from the NAT's client list entirely. A Lambda outside the VPC reaches AWS APIs directly, for free, and starts faster.

For the seven that need Aurora, there's a further option we're evaluating: RDS Data API, which lets a function outside the VPC query an Aurora cluster over HTTPS. It changes the driver, so it's not a flag, but it would empty the VPC of Lambdas entirely.

**4. Don't put one in dev.** The dev environment's NAT existed so that dev Lambdas could reach the internet. After step 3, the only things in dev's private subnets that needed the internet were the seven database functions fetching secrets and posting to queues. Two interface endpoints, Secrets Manager and SQS, at $15 a month, replaced a $33 NAT, and dev's third-party API calls (there aren't any in dev; they're stubbed) stopped needing a path at all. Staging got the same treatment.

**5. A NAT instance where you truly need cheap egress.** If dev *had* needed general internet access, the answer is a NAT instance rather than a NAT gateway: a `t4g.nano` running the open-source `fck-nat` image costs about $3 a month, handles a couple of hundred megabits, and is a fine trade for a non-production environment where a single point of failure is acceptable. We didn't need it, but it's the right tool for the "I need a NAT but not a $33 NAT" case. Never in production.

There's a sixth that only applies if you can go IPv6-only: an egress-only internet gateway is free and does for IPv6 what a NAT does for IPv4. Most third-party APIs aren't reachable over IPv6 yet, so it wasn't an option for us.

## Where we ended up

| | Before | After | Monthly |
|---|---|---|---|
| prod | 1 NAT gateway | 1 NAT gateway + S3/DynamoDB gateway endpoints | $36 → $36 |
| staging | 1 NAT gateway | Secrets Manager + SQS interface endpoints, no NAT | $33 → $15 |
| dev | 1 NAT gateway | Secrets Manager + SQS interface endpoints, no NAT | $33 → $15 |
| Lambdas in VPC | 12 | 7 | |
| **Total** | **$102** | | **$66** |

Production keeps its NAT: it calls third-party APIs, it needs a real path, and $36 for a managed, multi-AZ-capable component with no maintenance is fair. The saving is $36 a month, which sounds small until you notice it's 5 % of the whole bill, it took an afternoon, and the same afternoon made five Lambdas start faster.

<div class="article-figure">
<svg viewBox="0 0 900 200" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Before and after bars. Before: three NAT gateways at 33 dollars each, 102 dollars a month, 12 Lambdas in the VPC. After: one NAT gateway in production at 36 dollars, two interface endpoints each in dev and staging at 15 dollars each, total 66 dollars, 7 Lambdas in the VPC. Saving 36 dollars a month, 5 percent of the bill, one afternoon of work.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="24" fill="#f1f3ff" font-size="14" font-weight="700">NAT and endpoint cost, before and after</text>
<text x="20" y="66" fill="#9aa3c7">before</text>
<rect x="90" y="52" width="230" height="22" fill="#ff6b8a" opacity="0.85"/><text x="205" y="67" text-anchor="middle" fill="#0d1120" font-size="11">prod NAT $36</text>
<rect x="320" y="52" width="210" height="22" fill="#ff6b8a" opacity="0.65"/><text x="425" y="67" text-anchor="middle" fill="#0d1120" font-size="11">staging NAT $33</text>
<rect x="530" y="52" width="210" height="22" fill="#ff6b8a" opacity="0.45"/><text x="635" y="67" text-anchor="middle" fill="#0d1120" font-size="11">dev NAT $33</text>
<text x="750" y="67" fill="#ff6b8a" font-weight="700">$102</text>
<text x="20" y="116" fill="#9aa3c7">after</text>
<rect x="90" y="102" width="230" height="22" fill="#ff6b8a" opacity="0.85"/><text x="205" y="117" text-anchor="middle" fill="#0d1120" font-size="11">prod NAT $36</text>
<rect x="320" y="102" width="96" height="22" fill="#ffd166" opacity="0.85"/><text x="368" y="117" text-anchor="middle" fill="#0d1120" font-size="11">staging $15</text>
<rect x="416" y="102" width="96" height="22" fill="#ffd166" opacity="0.65"/><text x="464" y="117" text-anchor="middle" fill="#0d1120" font-size="11">dev $15</text>
<text x="522" y="117" fill="#4fffb0" font-weight="700">$66 · −$36 / month</text>
<text x="20" y="160" fill="#9aa3c7">Lambdas in the VPC: 12 → 7 · gateway endpoints added everywhere · one afternoon</text>
<text x="20" y="184" fill="#9aa3c7">The lever was the count of NATs, not the bytes through them.</text>
</g>
</svg>
</div>

## The rule for new things

Before a new function or service goes into a private subnet, it has to answer one question in the pull request: *what in the private subnet does it talk to?* If the answer is "nothing", it doesn't go in. If the answer is "the database", it goes in and it gets the endpoints it needs listed. If the answer is "it needs the internet", then it's production or it's a design conversation.

The NAT gateway isn't a bad product. It's a product that's easy to have three of without deciding to. Count yours.

If your EC2-Other line is bigger than your EC2 line, [we can tell you why in an afternoon](/contact).
