Public vs private subnet: it is the route table, not the name
There is no public checkbox. A subnet is public because of one line in its route table, and everything else people believe about the difference follows from that single fact.
Both instances below sit in the same VPC. Same account, same region, same security groups. Watch where an outbound packet actually goes.
One outbound packet, from each kind of subnet
1 / 5Instance in a public subnet
The subnet's route table sends 0.0.0.0/0 to the Internet Gateway. That single line is what makes it public.
Instance in a private subnet
This route table sends 0.0.0.0/0 to a NAT Gateway instead. The NAT Gateway itself sits in the public subnet. It has to, or it could not reach the IGW either.
Two instances in the same VPC. The only real difference is which subnet they sit in, and what that subnet's route table does with 0.0.0.0/0.
The three things this diagram settles
A "public subnet" is not a property of the subnet. It is the route table
associated with it. Move that association and the same subnet becomes private,
with no other change. This is why terraform plan showing a route table
association change is never cosmetic.
A NAT Gateway must live in a public subnet. People put it in the private subnet constantly, because that is where the instances needing it are. But NAT reaches the internet the same way anything else does (via the IGW), so it needs a route table pointing at one. Put it in the private subnet and you have built a loop: the route to the internet points at a device whose own route to the internet points at itself.
A public IP is necessary but not sufficient. An instance with a public IP in a subnet whose route table has no IGW entry is unreachable and cannot reach out. This is the single most common "my instance has a public IP but nothing works" cause.
| Public subnet | Private subnet | |
|---|---|---|
| What defines it | Route table sends 0.0.0.0/0 to an IGW. | Route table sends 0.0.0.0/0 to a NAT (or nowhere). |
| Outbound traffic | Direct, via the IGW. | Via a NAT Gateway that lives in a public subnet. |
| Inbound connections | Possible, if SGs and NACLs allow. | Impossible: NAT has no entry for unsolicited traffic. |
| What the internet sees | The instance's own public IP. | The NAT Gateway's IP, shared by every instance behind it. |
| Cost | IGW is free. | NAT Gateway: hourly charge plus per-GB processing. |
| Belongs there | Load balancers, bastions, NAT gateways. | App servers, databases, anything with no reason to be reachable. |
The cost trap worth knowing
The NAT Gateway is one of the most reliably surprising lines on an AWS bill: an hourly charge per gateway, plus a per-GB fee on everything passing through. A private subnet pulling container images or writing to S3 pays that fee on every byte.
The fix is a VPC endpoint: a route to S3, ECR or DynamoDB that stays on the AWS network and never touches the NAT at all. If a bill shows heavy NAT processing, endpoints are almost always the first thing to reach for.
Remember this
- 1A subnet is public if and only if its route table sends 0.0.0.0/0 to an Internet Gateway. There is no flag.
- 2The NAT Gateway lives in the public subnet, not the private one: it needs its own route to the IGW.
- 3NAT gives outbound-only access: replies to connections you started come back, but nothing can initiate a connection inward.