Introduction
Regional NAT Gateway is a relatively new feature announced on November 20, 2025. It is available in all commercial AWS Regions (except AWS GovCloud (US) and China Regions).
Right after launch, the community got excited: "Now I don't have to create public subnets," "Route table management gets dramatically simpler." A lot of verification posts went around.
Roughly half a year has passed. The launch buzz has settled, and I wanted to put my own hands on it again. Specifically, I wanted to nail down the question: can you really stand up a VPC with zero public subnets? — not just by what the console looks like, but by CLI responses and actual outbound traffic from private EC2 instances. This article is that verification log.
This article is not an introduction to Regional NAT Gateway. It is a record of facts I verified myself. For comprehensive spec coverage, see the official documentation.
Test environment
| Item | Value |
|---|---|
| Region | ap-northeast-1 (Tokyo) |
| Date | 2026-06-28 |
| AWS CLI | aws-cli/2.32.6 |
| VPC CIDR | 10.20.0.0/16 |
| Subnets | Private only, 2 subnets (10.20.1.0/24 @ 1a, 10.20.2.0/24 @ 1c) |
| Internet Gateway | Present (attached to VPC only; no route from any subnet) |
| Public subnets | 0 (never created) |
| NAT Gateway | Regional (Automatic mode), 1 |
| EC2 | Amazon Linux 2023 × 2 (one each in 1a / 1c, accessed via SSM Session Manager) |
To make the verification honest, I imposed these constraints:
- Never create a public subnet (Tier=private only, MapPublicIpOnLaunch=false)
- Never add an IGW route to a subnet's route table (the moment you do, it's a public subnet)
- Never specify a subnet ID when creating the NAT Gateway (that's the whole point of regional mode)
Architecture
The point: the 0.0.0.0/0 → IGW route only exists in the NAT GW's auto-generated route table. No subnet-attached route table mentions the IGW. That's exactly why we can claim "no public subnet exists."
Steps and verifications
1. Create the VPC and private subnets
Plain VPC, two private subnets. I don't explicitly set MapPublicIpOnLaunch (it defaults to false).
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.20.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=rnat-validation}]' \
--query 'Vpc.VpcId' --output text)
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support
aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.20.1.0/24 \
--availability-zone ap-northeast-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=rnat-private-1a},{Key=Tier,Value=private}]'
aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.20.2.0/24 \
--availability-zone ap-northeast-1c \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=rnat-private-1c},{Key=Tier,Value=private}]'
2. Attach (only) an Internet Gateway
This is the big departure from convention.
The old pattern: "Create an IGW → add 0/0 → IGW to the public subnet's route table."
With Regional NAT Gateway, you only attach the IGW to the VPC. Don't touch any subnet's route table.
IGW_ID=$(aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=rnat-igw}]' \
--query 'InternetGateway.InternetGatewayId' --output text)
aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID
3. Create the Regional NAT Gateway
The heart of the verification. Do not pass any subnet ID. Pass only --availability-mode regional and --vpc-id.
aws ec2 create-nat-gateway \
--vpc-id $VPC_ID \
--availability-mode regional \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=rnat-regional}]'
Response (excerpt):
{
"NatGateway": {
"NatGatewayId": "nat-195a71ed93ed09728",
"VpcId": "vpc-0e97b9be61166b6f5",
"ConnectivityType": "public",
"AvailabilityMode": "regional",
"AutoScalingIps": "enabled",
"AutoProvisionZones": "enabled",
"NatGatewayAddresses": [],
"State": "pending"
}
}
AvailabilityMode: regional / AutoScalingIps: enabled / AutoProvisionZones: enabled. Creation in Automatic mode succeeded. The remarkable part: there is no SubnetId in the response at all.
After a few dozen seconds it became available. Here is describe-nat-gateways:
{
"NatGatewayId": "nat-195a71ed93ed09728",
"State": "available",
"VpcId": "vpc-0e97b9be61166b6f5",
"NatGatewayAddresses": [
{
"AllocationId": "eipalloc-0597c57299628bded",
"PublicIp": "57.182.69.198",
"AvailabilityZone": "ap-northeast-1d",
"AvailabilityZoneId": "apne1-az2",
"Status": "succeeded"
}
],
"AvailabilityMode": "regional",
"AutoScalingIps": "enabled",
"AutoProvisionZones": "enabled",
"RouteTableId": "rtb-07a53bf85bfebb378"
}
Two important facts here:
- A dedicated route table (
RouteTableId: rtb-07a53bf85bfebb378) was automatically generated for the NAT Gateway. - My subnets are in
1aand1c, but the first EIP was placed inap-northeast-1d. With no workload ENIs yet, AWS picks some AZ to plant a single "foothold."
4. Look inside the NAT GW's auto-generated route table
describe-route-tables on rtb-07a53bf85bfebb378:
{
"RouteTableId": "rtb-07a53bf85bfebb378",
"VpcId": "vpc-0e97b9be61166b6f5",
"Associations": [
{
"RouteTableAssociationId": "rtbassoc-0cec892ccb8340f3b",
"GatewayId": "nat-195a71ed93ed09728",
"AssociationState": {"State": "associated"}
}
],
"Routes": [
{"DestinationCidrBlock": "10.20.0.0/16", "GatewayId": "local", "State": "active"},
{"DestinationCidrBlock": "0.0.0.0/0", "GatewayId": "igw-08bce51fdce77bd7c", "Origin": "CreateRoute", "State": "active"}
]
}
What we can see:
- This route table is edge-associated to the NAT Gateway itself, not to any subnet (
Associations[].GatewayIdis the NAT GW ID). -
0.0.0.0/0 → IGW has been auto-injected (
Origin: CreateRoute).
This is the core of "no public subnet required." Subnets no longer need to carry an IGW-bound route, because the NAT GW itself owns a private route table that talks directly to the IGW.
5. The private subnets' route table
The subnet-side route table needs only 0.0.0.0/0 → NAT GW.
PRIV_RTB=$(aws ec2 create-route-table --vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=rnat-private-rtb}]' \
--query 'RouteTable.RouteTableId' --output text)
aws ec2 create-route --route-table-id $PRIV_RTB \
--destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-195a71ed93ed09728
aws ec2 associate-route-table --route-table-id $PRIV_RTB --subnet-id subnet-...1a
aws ec2 associate-route-table --route-table-id $PRIV_RTB --subnet-id subnet-...1c
The key point: one RTB serves both 1a and 1c. In the classic zonal NAT pattern you'd build a NAT per AZ, build a route table per AZ, and write a separate 0/0 → AZ-specific NAT for each — repeated for every AZ. With Regional NAT Gateway, adding AZs requires no RTB changes.
Three route tables in the VPC:
-
rnat-private-rtb: Explicit subnet associations = 2 subnets (one RTB covers both 1a and 1c) - An unnamed Main RTB (VPC default, unused)
- An unnamed auto-generated RTB: Edge associations column shows
nat-195a71ed93ed...
Having a NAT GW ID in the "Edge associations" column is the visual signature of this new feature.
6. Does a private subnet actually reach the internet?
I SSH'd into the 1a EC2 with SSM Session Manager and hit https://checkip.amazonaws.com.
=== my source IP seen by the internet ===
57.182.69.198
=== resolve test ===
HTTP/2 405
=== route ===
default via 10.20.1.1 dev ens5 proto dhcp src 10.20.1.78 metric 512
10.20.0.2 via 10.20.1.1 dev ens5 proto dhcp src 10.20.1.78 metric 512
10.20.1.0/24 dev ens5 proto kernel scope link src 10.20.1.78 metric 512
=== metadata az ===
ap-northeast-1a
- The EC2's local IP is
10.20.1.78(in 1a). - The source IP seen from the internet is
57.182.69.198, matching the NAT GW's EIP. -
HTTP/2 405is a normal response fromhttps://www.amazon.co.jp/tocurl -I(amazon.co.jp doesn't allow HEAD; meaning TCP/TLS is established = external reachability is fine).
I ran the same script from the 1c EC2 — same 57.182.69.198. The 1c EC2 is in ap-northeast-1c, but the NAT GW currently has a single foothold in ap-northeast-1d. The docs say "until the expansion completes, traffic is processed in an existing AZ," and we can see that with our own eyes.
7. A formal proof that no public subnet exists
"Subjectively, I didn't create any" is too weak. Let's prove it formally via the API.
Subnets:
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[].{Subnet:SubnetId,AZ:AvailabilityZone,CIDR:CidrBlock,MapPublicIpOnLaunch:MapPublicIpOnLaunch,Tier:Tags[?Key==`Tier`]|[0].Value}'
| AZ | CIDR | MapPublicIpOnLaunch | Tier |
|---|---|---|---|
| ap-northeast-1c | 10.20.2.0/24 | false | private |
| ap-northeast-1a | 10.20.1.0/24 | false | private |
Then count how many route tables are directly associated to a subnet AND have 0.0.0.0/0 pointing to an IGW — that is exactly the definition of "public subnet."
aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" --output json | \
jq -r '[.RouteTables[]
| select(.Routes[]? | select(.DestinationCidrBlock=="0.0.0.0/0"
and (.GatewayId//"" | startswith("igw-"))))
| .Associations[]
| select(.SubnetId != null)
| .SubnetId] | "public_subnet_count=" + (length|tostring)'
Result:
public_subnet_count=0
Zero route tables carry an IGW-bound 0/0 while being attached to a subnet. The only RTB with an IGW route, rtb-07a53bf85bfebb378, is edge-associated to the NAT Gateway itself — not to any subnet.
Inside the NAT GW's auto-generated RTB. The Routes tab shows 0.0.0.0/0 → igw-... injected as Create Route. The Edge associations tab lists the NAT GW. Explicit subnet associations are, as expected, "-" (none).
For completeness, the subnets list:
Only rnat-private-1a and rnat-private-1c. The VPC column shows both belong to the same VPC. Plain and simple.
8. Multi-AZ auto-expansion behavior
After launching an EC2 in 1c, I polled describe-nat-gateways once per minute to watch the expansion. The official docs say new-AZ expansion takes up to 60 minutes.
while true; do
date -u +%H:%M:%SZ
aws ec2 describe-nat-gateways --nat-gateway-ids nat-195a71ed93ed09728 \
--query 'NatGateways[0].NatGatewayAddresses[].{AZ:AvailabilityZone,IP:PublicIp}' \
--output text
sleep 60
done
Timeline:
| Time (UTC) | Elapsed | Addresses | Breakdown |
|---|---|---|---|
| 08:33:27 | 0:00 | 1 | 1d=57.182.69.198 (Succeeded) |
| 08:33:27 | 0:00 | ↑ EC2 launched in 1c (expansion trigger) | |
| 08:35:01 | +1:34 | 1 | still 1d only |
| 08:49:14 | +15:47 | 3 | 1d=...198 (Succeeded) / 1c=null (Associating) / 1a=null (Associating) |
| 08:55:19 | +21:52 | 3 | 1d=...198 / 1c=52.193.167.188 / 1a=54.249.235.126, all Succeeded |
| 09:33:54 | +60:27 | 2 | 1a=...126 / 1c=...188 only. The 1d-side EIP, with no ENI, was auto-contracted. |
Entries added in ~16 minutes, EIP allocation finished in ~22 minutes. Much faster than the 60-minute upper bound (just this single observation, of course).
I only added an EC2 in 1c, yet 1a was also expanded at the same time. That's because 1a already had its own EC2 — the NAT GW expands to every AZ that has a workload ENI, not just the AZ that triggered the event.
And about 60 minutes after launch, the EIP in 1d (which has no ENI) was automatically contracted. 1a and 1c stayed because they have ENIs.
This is the live observation of "contracts from the Availability Zone that has no active workloads" — auto-expansion is matched by auto-contraction.
During expansion (Associating):
I caught the moment when apne1-az2 (1d) was already Succeeded while apne1-az1 (1c) and apne1-az4 (1a) were still Associating.
After expansion:
All three AZs got distinct EIPs, all Succeeded.
Then I re-checked source IPs from each AZ's EC2:
| EC2 AZ | Private IP | Source IP seen from internet | Matching NAT GW EIP (same AZ) |
|---|---|---|---|
| ap-northeast-1a | 10.20.1.78 | 54.249.235.126 | 54.249.235.126 (apne1-az4) |
| ap-northeast-1c | 10.20.2.x | 52.193.167.188 | 52.193.167.188 (apne1-az1) |
After expansion, each AZ's EC2 uses the EIP placed in its own AZ (zonal affinity is established). The same single NAT GW let me observe both the pre-expansion "cross-AZ processing" state and the post-expansion "zonal affinity" state.
Assessment
Notes from the verification.
What I liked
- The setup commands are genuinely short. Not having to specify a subnet when creating the NAT GW had more impact than I expected.
- A whole class of mistakes ("oops, I put a private instance in a public subnet") becomes structurally impossible. The security upside is real.
- No more "create-NAT-rewrite-route-table" dance when you add an AZ.
- A single NAT GW ID covers every AZ's subnets — even your IaC stops needing per-AZ loops.
What to watch out for
- AZ expansion is officially "up to 60 minutes" — in my run, ~22 minutes. Not for "process traffic from another AZ right now" requirements (until expansion completes, traffic is cross-AZ processed).
- Private NAT use cases (VPC→VPC NAT) are not supported. You still need zonal NAT for that.
- Migration from existing zonal NAT involves connection reset. Plan a maintenance window.
- Pricing is on the same model as zonal NAT (hourly + data processing). "Consolidating into one NAT" does not lower the unit cost.
What I haven't tried yet (candidates for next time)
- Behavior with Transit Gateway in the route path
- How fast it follows a high rate of expansion/contraction events in a short window
- Real-world operational feel of running zonal and regional side-by-side in the same VPC
Closing
Half a year after launch, building this from scratch made one thing concrete: "a VPC with no public subnets" is no longer a special construction — it's the path of least resistance.
When designing a new VPC, the natural ordering is now: "Think Regional NAT GW first. Fall back to zonal only when something forces it (private NAT needed, 60-minute expansion is unacceptable, etc.)."
The day "zero public subnets" stops being a clever trick and becomes the default isn't far off.
This verification gave me solid footing to say that.
References
- What's New: AWS NAT Gateway now supports regional availability (2025-11-20)
- Networking & Content Delivery Blog: Introducing Amazon VPC Regional NAT Gateway
- User Guide: Regional NAT gateways for automatic multi-AZ expansion
Resources used in this verification (for cleanup reference)
| Type | ID |
|---|---|
| VPC | vpc-0e97b9be61166b6f5 |
| Subnet (1a) | subnet-003c9aaac5dbd023a |
| Subnet (1c) | subnet-0ff0bfe6aa12d2960 |
| IGW | igw-08bce51fdce77bd7c |
| Regional NAT GW | nat-195a71ed93ed09728 |
| Private RTB | rtb-01d2fb6653a7e4f09 |
| Auto-generated RTB (NAT GW edge) | rtb-07a53bf85bfebb378 |
| Security Group | sg-0654f18b5120145dd |
| EC2 (1a) | i-00aa80bb16f6f92a2 |
| EC2 (1c) | i-0f88674d033fdd78d |
| IAM Role / Profile | rnat-ssm-role / rnat-ssm-profile |
| EIP allocation | eipalloc-0597c57299628bded |






Top comments (0)