Why architecture matters here

The NAT gateway matters because it is the default answer to a near-universal requirement — 'my private servers need outbound internet' — and the naive alternatives are all worse. Giving each private instance a public IP re-exposes the inbound surface you were trying to eliminate and multiplies the addresses an attacker can probe. Running your own NAT instance on a VM means you own its availability, patching, and scaling, and a single NAT instance is a single point of failure and a throughput bottleneck. The managed gateway exists so that outbound access is a configuration choice, not an operational burden or a security regression.

It matters for security posture specifically because of the directionality. A private subnet routed through a NAT gateway can reach out but cannot be reached; there is no listener, no inbound rule, and no mapping an external scanner could hit. This lets you keep the vast majority of your compute off the public internet entirely while still letting it function. Combined with egress controls — security groups, and increasingly egress firewalls or proxies in front of the gateway — it becomes the choke point where you can observe and constrain what your fleet talks to.

It matters for availability because NAT gateways are zonal resources. A gateway lives in one availability zone, and if that zone has a problem, that gateway is gone. A design that routes every private subnet through a single gateway in one AZ has quietly made every zone depend on one zone for outbound connectivity — so a single-AZ event takes out egress fleet-wide. The correct pattern is one gateway per AZ with each zone's private subnets routing to their local gateway, so a zone failure is contained to that zone. Getting this wrong is one of the most common cloud-architecture mistakes, and it only reveals itself during an outage.

Finally it matters for cost, because the NAT gateway is metered in a way that surprises people. You pay an hourly rate for each gateway plus a per-gigabyte charge on all data processed through it. Traffic that could have stayed inside the cloud — reaching a storage service, a container registry, or another service in the same region — instead routes out through the NAT and racks up processing charges, when a VPC endpoint or gateway endpoint could have kept it on the provider's backbone for free or far cheaper. The NAT gateway's cost model turns architecture decisions about where traffic flows into line items on the bill, which is why understanding it is a FinOps concern, not just a networking one.

Advertisement

The architecture: every piece explained

The private subnet is where the workloads live: instances, containers, and functions with no public IP. They are invisible to the internet by construction. Their only path outward is whatever their route table says.

The route table is the steering mechanism. A private subnet's route table sends its default route (0.0.0.0/0) to the NAT gateway rather than to an internet gateway. That single line is what makes the subnet 'private with egress': packets destined for the internet go to the NAT, packets for local VPC ranges stay local, and there is no inbound route at all.

The NAT gateway itself sits in a public subnet — a subnet whose route table does point at the internet gateway — and holds an elastic (static public) IP. When it receives a packet from a private instance, it rewrites the source address to its own public IP and rewrites the source port to a value it allocates from its ephemeral port pool, recording the mapping in a connection-tracking table. The reply comes back addressed to the gateway's public IP and the allocated port; the gateway looks up the mapping and rewrites the destination back to the original private instance and port. The internet gateway is the actual door to the public internet that the NAT's traffic egresses through.

Port translation and the ephemeral port pool are the capacity constraint. Each unique outbound connection to a given destination (destination IP and port) needs a distinct source port on the gateway's public IP, and there are only around 55,000 usable ephemeral ports per destination tuple per public IP. For most workloads that is plentiful, but a fleet making a very large number of simultaneous connections to the same destination — a common pattern when everything talks to one API or one database endpoint — can exhaust the pool for that destination and start failing to allocate ports, which surfaces as connection errors.

Per-AZ deployment is the availability structure. You create one NAT gateway in each availability zone, in that zone's public subnet, and point each zone's private subnets at their own local gateway. This makes egress zonal: if one AZ fails, only that zone's private workloads lose their (local) gateway, and the other zones keep egressing through their own. The alternative — all zones sharing one gateway — couples every zone's egress to one zone's fate and also incurs cross-AZ data charges on the traffic that hops zones to reach the shared gateway.

Managed NAT gateway — outbound internet for private subnets, no inbound reachabilitymany private instances share a few public source addressesPrivate subnetinstances with no public IPNAT gateway (per-AZ)in a public subnet, elastic IPInternet gatewayegress to public internetRoute table0.0.0.0/0 -> NAT gatewayPort translationsrc IP:port rewrite + conntrackEphemeral port pool~55k per destination tupleOne NAT per AZzonal fault isolationMetered — data processed + hourscost driver for chatty egressResult — private workloads reach the internet outbound-only, without exposing an inbound surfacedefault routerewriteallocatematchassignper zonebillegressegress
A managed NAT gateway sits in a public subnet with an elastic IP; private subnets route their default route to it, and it rewrites source addresses and ports so many private instances share a handful of public IPs — outbound only.
Advertisement

End-to-end flow

Follow a request from a private application server pulling a container image. The server opens a TCP connection to a public registry endpoint. Its subnet's route table matches the destination against 0.0.0.0/0 and forwards the packet to the local NAT gateway. The gateway rewrites the source from the instance's private IP to its own elastic public IP, allocates a free ephemeral source port, and records the mapping (private IP:port to public IP:port for that destination) in its connection-tracking table.

The rewritten packet leaves through the internet gateway and reaches the registry, which sees the request as coming from the NAT's public IP. The registry's response is addressed back to that public IP and the allocated port. The NAT gateway receives it, looks up the connection-tracking entry, rewrites the destination back to the original private instance and port, and forwards it into the private subnet. The application server receives the reply as if it had a direct route out, never knowing its address was translated.

Throughout the connection's life the mapping persists in the tracking table, so all packets of that TCP flow are translated consistently. When the connection closes (or times out from inactivity), the mapping is released and the ephemeral port returns to the pool for reuse. Under normal load thousands of such flows coexist, each with its own mapping, sharing the gateway's single public IP through distinct source ports.

Now consider the availability dimension in the same flow. Because the server routed to the gateway in its own AZ, all of this stayed within one zone until the packet hit the internet gateway. If that AZ were to fail, the server would fail with it — but servers in other AZs, routing to their own local gateways, would be entirely unaffected. And if the destination had been an in-region service with a VPC endpoint, the route table could have sent the traffic to that endpoint instead, keeping it on the provider backbone and off the metered NAT path entirely — the same request, a very different cost and blast radius.