The terms "public subnet" and "private subnet" are deeply misleading. Both kinds of AWS subnet are technically identical — same CIDR, same VPC, same reserved IPs, same maximum size. The only difference is in the route table: a public subnet has a route to the Internet Gateway; a private subnet does not.
This article explains exactly what those two flavors are good for, the design patterns for combining them in production, and the common confusions that lead to security issues.
What makes a subnet "public"
A public subnet has these characteristics:
- Its route table contains a route for
0.0.0.0/0pointing to an Internet Gateway. - Instances in it can have public IP addresses or Elastic IPs assigned to their ENIs.
- Inbound traffic from the internet can reach instances if security groups and NACLs allow it.
That is the entire definition. There is no "public subnet" flag in the AWS API — it is just a subnet whose route table happens to have an Internet Gateway route.
What makes a subnet "private"
A private subnet has these characteristics:
- Its route table has no direct route to the Internet Gateway.
- Outbound internet traffic goes through a NAT Gateway, NAT Instance, or VPC Endpoint (or not at all).
- Inbound traffic from the internet cannot reach instances even if security groups allow it (there is no return path).
Again, no "private subnet" flag — it's just the absence of an Internet Gateway route.
The standard production layout
The pattern that 95% of production AWS workloads use:
VPC: 10.0.0.0/16 +-- Public subnet az-a 10.0.0.0/24 | - ELB (ALB / NLB) | - NAT Gateway | - Bastion host (optional) +-- Public subnet az-b 10.0.1.0/24 | - (ELB targets here too) | - NAT Gateway +-- Public subnet az-c 10.0.2.0/24 | - NAT Gateway +-- Private subnet az-a 10.0.16.0/20 | - EC2 application instances | - EKS nodes +-- Private subnet az-b 10.0.32.0/20 +-- Private subnet az-c 10.0.48.0/20 +-- DB subnet az-a 10.0.64.0/24 | - RDS +-- DB subnet az-b 10.0.65.0/24 +-- DB subnet az-c 10.0.66.0/24
The three layers:
- Public subnets: hold load balancers and NAT Gateways. Small (/24 is plenty).
- Private subnets: hold application workloads. Large (/20 typical for ENI-heavy workloads).
- DB subnets: hold RDS, ElastiCache. Small (/24). Often a separate "tier" with stricter security groups.
The ELB sits in public subnets and forwards traffic to targets in private subnets. The targets cannot accept direct internet connections, but the ELB can. This is the standard pattern.
Why this layout works
- Security: Application code in private subnets has no return path from the internet, so even if an instance is compromised, attackers cannot send back exfiltrated data over the same TCP connection (they would need to initiate an outbound connection through NAT).
- Operational simplicity: Only the ELB needs to be hardened against direct internet attacks. Application instances rely on the ELB's WAF and rate limiting.
- NAT cost containment: Application instances do not need static public IPs — they share the NAT Gateway's address pool.
When to use a public subnet
Only the following resources belong in public subnets:
- Load balancers (ALB, NLB)
- NAT Gateways
- Bastion hosts (optional, increasingly replaced by SSM Session Manager)
- VPN gateways / Site-to-Site VPN endpoints
- Internet-facing reverse proxies (when ALB is not sufficient)
If you find yourself putting application servers in a public subnet, stop. Even with strict security groups, the architecture creates risk surface. Move them behind a load balancer.
When to use a private subnet
Almost everything else:
- Application EC2 instances
- EKS nodes
- RDS / ElastiCache (sometimes in a separate DB-only subnet for stricter isolation)
- Lambda functions (when VPC-attached)
- Fargate tasks
- Internal services not reachable from outside the VPC
Bastion hosts: increasingly optional
Historically, the only way to SSH into a private-subnet instance was through a public-subnet bastion host. That pattern is still common but increasingly unnecessary.
AWS Systems Manager Session Manager allows SSH-equivalent access to instances without any public-subnet hop. The instance just needs the SSM agent and an IAM role permitting SSM. No bastion, no public IPs, no inbound SSH rules.
For production workloads in 2026, the recommended pattern is:
- SSM Session Manager for ad-hoc instance access
- AWS Client VPN or third-party VPN for engineer LAN access
- No bastion hosts in public subnets
"Egress-only" private subnets
If you want even less network exposure, make the subnet "egress-only" by removing even the NAT route. Workloads in such a subnet can only reach:
- Other resources in the same VPC
- VPC endpoints (S3, DynamoDB, PrivateLink to other AWS services)
- Peered VPCs (if peering is configured)
This is useful for compliance-heavy workloads where outbound internet is forbidden. Combined with VPC endpoints for AWS services, applications can still function without any internet access at all.
Common public/private subnet mistakes
- Putting application instances in public subnets "to make things simpler." Almost never the right tradeoff. The architectural simplicity is not worth the security risk.
- Confusing "private" with "secure." A private subnet without proper security groups is just as exploitable as a public one if an attacker gets in some other way (e.g., a compromised instance in the same VPC). Security groups still matter.
- Forgetting that NAT Gateways are in public subnets. A common surprise: "Why is my NAT bill so high?" because the NAT Gateway sits in a public subnet and processes all outbound traffic.
- Cross-AZ NAT routing. If your private subnet in AZ-A routes to a NAT Gateway in AZ-B, every byte crosses AZs. Always per-AZ NAT.
The Azure and GCP equivalents
Azure and GCP do not use the "public" / "private" terminology officially, but the same pattern applies:
- Azure: a subnet becomes "public" by associating it with an Azure Public IP or NAT Gateway. Otherwise it's private.
- GCP: VMs with public IPs are reachable from the internet; VMs without public IPs need Cloud NAT for outbound traffic. The same architectural patterns apply.
Key takeaways
- "Public" subnet = subnet with a default route to an Internet Gateway. "Private" subnet = no Internet Gateway route.
- Standard production layout: public for ELB + NAT, private for apps, separate DB tier.
- Use SSM Session Manager instead of bastion hosts.
- Workloads with outbound internet needs use a NAT Gateway in their AZ. See our NAT options article for cost optimization.
- "Egress-only" subnets with only VPC endpoints are the most restrictive pattern, used for compliance-heavy workloads.