Stop using private subnets, eliminating NAT costs with IPv6 in AWS
Using NAT is the default pattern for enabling egress traffic for resources in a private network. Indeed, it's a requirement when resources don't get IP addresses that are routable from the public internet, e.g. the IPv4 private ranges 10.0.0.0/8 and 172.16.0.0/12. NAT is pervasive in examples, blogs, how-tos, and is often present in "best practice" architectures when separating private and public resources.
Take the standard AWS VPC design:

Being a fairly typical starting point, it presents two AZs separated into public and private subnets, with NAT Gateways to enable egress. But as has been covered more times than I care to count, NAT can become very expensive very quickly. A single NAT Gateway is about $32.50/mo, but introduces another trap in the form of a $0.048/GB "processing fee" which nullifies the common belief that traffic "into the cloud" is free. For our above two AZ example and a modest combined 10TB of ingress/egress traffic that adds up to approx. $6,740/y.
AWS also provides AMIs for NAT instances, but they're arguably not worth the headache. While being potentially cheaper per hour, AWS's own comparison makes it pretty clear it instead becomes expensive from an operational perspective.
The cost of NAT is a tough pill to swallow just to let some resources that happen to have IPs from the private range be able to do basic things like serve traffic and receive updates. Which brings us to the core question: do we really need private subnets in modern architectures?
Stuck in old thinking with IPv4
Besides private subnets being cargo culted from traditional on-premise network design, the primary reason for network segmentation into private subnets is of course that we (almost 15 years ago at the time of this writing) have run out of IPv4 addresses, turning them into a precious commodity. AWS has been charging for unused IPv4 addresses for a long time, but since Feb '24 you'll be charged for any provisioned public IPv4 address at ~$3.65. Even a moderately sized AWS-based organization with around 600 IPs allocated for things like EC2 instances, ALBs, NLBs, Lambdas in VPC with public ENI, and accidentally deployed resources (EKS nodes with accidental elastic IPs, anyone?) in public subnets would result in a monthly bill of $2,190 in just address fees.
Which of course begs the question, why do we need to default to IPv4 in 2025?

It's about damn time to get started with IPv6
The solution to NAT Gateway and IPv4 question is, as most can probably guess, IPv6. Globally routable by design, NAT doesn't even exist as a concept in an IPv6-only topology. For AWS VPC, the standard Internet Gateway (IGW) is the same as for IPv4, but with IPv6 we also get access to an Egress-only Internet Gateway (EIGW) which, as the name implies, only allows one-way traffic. The major difference compared to NAT Gateway is that EIGW is free and doesn't have the "processing fee", meaning we're back to only paying for egress data.
While IPv6 is very mature at this stage, and a lot of AWS services support IPv6 natively, we need to recognize that some things still require IPv4. This is achieved through "dualstack" VPCs, creating subnets which are able to provide resources with both IPv4 and IPv6. Providing subnet resources with both is straight forward:
- Enable dualstack on the VPC and subnets. This gives each deployed resource both an IPv4 and IPv6 address.
- Make sure the subnets don't have "public address" enabled by default, this makes sure we're not paying for IPv4 addresses unnecessarily.
- Enable DNS64 so IPv6 hosts can reach IPv4-only destinations via AWS’s built-in (free) NAT64.
- Only assign public IPv4s to resources that truly need them (e.g., ALBs).
- Use VPC endpoints when warranted, e.g. lower latency (DynamoDB), reduced data transfer cost (S3), or when an AWS service doesn't support IPv6 directly (SSM). You don't need to default to enabling endpoints for private subnets anymore.
This provides a simple pattern to let IPv6 be the default for egress traffic, IPv4 to enable internal AWS compatibility, make many VPC endpoints optional (they also cost money). But more importantly it completely eliminates NAT related costs from our bill.

Emulating Internet Gateway and NAT private/public behavior with IPv6 EIGW
A side effect of using NAT is blocking unsolicited inbound connections while still allowing outbound traffic. This is considered a security feature by many. The EIGW available in dualstack setups provides the same basic functionality. When reviewing the subnet topology the use of EIGW becomes apparent as it carries its own style of resource ID. As an example, if we assume that resources aren't given public IPv4 addresses by default, we can recreate the public/private network topology and say that we want IPv6 to be "private" but let IPv4 resources be "public":
0.0.0.0/0 -> igw-xxxxxxxx(For our public-facing IPv4 only resources)::/0 -> eigw-xxxxxxxx(For our "private" instances)
This pattern communicates a clear intent of only letting IPv4 addresses in the publicly addressable space be allowed incoming traffic. Exposing a host to the internet suddenly requires deliberately action by assigning an elastic IP; the automatically assigned IPv4 and IPv6 addresses would be protected from inbound connections.
Hot take: "private subnets" are not a meaningful security layer in modern cloud network topology
Now we're stepping into "that's just like, your opinion man" territory but I want to make a case for why the cargo culting of "best practices" and "extra isolation" makes little sense for modern IPv6-centered VPC design.
- In a modern VPC where all resources should be granted IAM RBAC least privilege, access should be tightly controlled with IaC methods such as CDK
grants, not by network isolation. - "Private networks" provide a false sense of security, more often than not some sort of bastion host is introduced which becomes an additional management point and cause of security concern.
- The majority of attacks on AWS infrastructure don't target internet-facing resources directly, instead attempting credential theft through malicious dependencies, app-layer exploits, or trawling for accidentally commited keys. Stolen credentials bypass the subnet boundaries entirely.
- The argument of limited "blast radius" becomes weak when compromised resources in public/private subnets have improper access controls. Proper controls apply the same level of protection regardless of whether the IPs attached are publicly routed or not.
Most attacks posing real threats care little about what type of subnet is used, and do nothing to prevent poorly scoped credentials, misconfigurations, and supply chain vulnerabilities. Switching to an IPv6 dualstack encourages defense-in-depth instead of us being lulled into a false sense of security with public/private subnet segmentation.
We have programmed our brains to equate networks with IPv4 for too long. Challenging myself to utilize IPv6 was not as big of a challenge as I first thought, and dualstack provides a convenient escape hatch for the rare occurrence when something requires IPv4. I dare you to give it a try, enable dualstack and EIGW and see what breaks. You'll be surprised not only what a small effort it is, but also what a huge impact removing the line items of NAT and elastic IPs from your bill will have.