AWS Subnets: Public, Private, and Isolated — How Traffic Actually Flows
A subnet is a range of IP addresses within your VPC, tied to a single Availability Zone and associated with a route table. The route table is what actually determines whether a subnet is “public” or “private” — there is nothing inherently different about the subnet resource itself. A subnet is public if its associated route table has a route pointing 0.0.0.0/0 at an Internet Gateway.
Understanding subnets means understanding how routing works at the network level, not just clicking checkboxes in the console.
The Routing Distinction That Matters
Public Subnet Route Table: 10.0.0.0/16 → local 0.0.0.0/0 → igw-0abc123 (Internet Gateway)
Result: instances can send/receive internet traffic IF they have a public IP
Private Subnet Route Table: 10.0.0.0/16 → local 0.0.0.0/0 → nat-0def456 (NAT Gateway in public subnet)
Result: instances can initiate outbound internet connections; no inbound
Isolated Subnet Route Table: 10.0.0.0/16 → local
Result: no internet in any direction; only VPC-internal trafficThe route table association is the only thing that distinguishes these subnet types. You can move a subnet from private to public by changing its route table.
Multi-AZ Subnet Design
Subnets span a single AZ. For high availability, you need subnets in at least two AZs. The standard production pattern uses three subnet tiers across two or three AZs:
VPC 10.0.0.0/16│├── AZ us-east-1a│ ├── 10.0.1.0/24 public-1a (ALB, NAT Gateway)│ ├── 10.0.11.0/24 private-1a (EC2, ECS tasks, Lambda)│ └── 10.0.21.0/24 isolated-1a (RDS, ElastiCache)│├── AZ us-east-1b│ ├── 10.0.2.0/24 public-1b (ALB, NAT Gateway)│ ├── 10.0.12.0/24 private-1b (EC2, ECS tasks, Lambda)│ └── 10.0.22.0/24 isolated-1b (RDS Multi-AZ standby)│└── AZ us-east-1c ├── 10.0.3.0/24 public-1c ├── 10.0.13.0/24 private-1c └── 10.0.23.0/24 isolated-1cIf one AZ goes down, resources in the other AZs continue serving traffic. The ALB redirects away from unhealthy targets automatically.
Creating Subnets and Route Tables
# Create VPCVPC_ID=$(aws ec2 create-vpc \ --cidr-block 10.0.0.0/16 \ --query 'Vpc.VpcId' --output text)
# Create public subnet in AZ-1aPUB_SUBNET_1A=$(aws ec2 create-subnet \ --vpc-id $VPC_ID \ --cidr-block 10.0.1.0/24 \ --availability-zone us-east-1a \ --query 'Subnet.SubnetId' --output text)
# Enable auto-assign public IP for public subnetsaws ec2 modify-subnet-attribute \ --subnet-id $PUB_SUBNET_1A \ --map-public-ip-on-launch
# Create private subnet in AZ-1aPRIV_SUBNET_1A=$(aws ec2 create-subnet \ --vpc-id $VPC_ID \ --cidr-block 10.0.11.0/24 \ --availability-zone us-east-1a \ --query 'Subnet.SubnetId' --output text)
# Create Internet Gateway and attachIGW_ID=$(aws ec2 create-internet-gateway \ --query 'InternetGateway.InternetGatewayId' --output text)aws ec2 attach-internet-gateway \ --internet-gateway-id $IGW_ID \ --vpc-id $VPC_ID
# Create public route tablePUB_RTB=$(aws ec2 create-route-table \ --vpc-id $VPC_ID \ --query 'RouteTable.RouteTableId' --output text)aws ec2 create-route \ --route-table-id $PUB_RTB \ --destination-cidr-block 0.0.0.0/0 \ --gateway-id $IGW_IDaws ec2 associate-route-table \ --subnet-id $PUB_SUBNET_1A \ --route-table-id $PUB_RTBNAT Gateway for Private Subnet Outbound Access
Private subnets can initiate outbound internet connections through a NAT Gateway. The NAT Gateway lives in a public subnet and has an Elastic IP.
Application server (10.0.11.45, private subnet) │ ▼ 0.0.0.0/0 route in private route tableNAT Gateway (10.0.1.10, public subnet, EIP: 54.123.45.67) │ ▼ Internet GatewayInternet (destination sees source IP as 54.123.45.67)The internet sees the NAT Gateway’s Elastic IP, not the private instance’s IP. Inbound connections to that Elastic IP do not reach private instances — NAT only handles outbound-initiated flows.
# Allocate Elastic IPEIP_ALLOC=$(aws ec2 allocate-address \ --domain vpc \ --query 'AllocationId' --output text)
# Create NAT Gateway in public subnetNAT_ID=$(aws ec2 create-nat-gateway \ --subnet-id $PUB_SUBNET_1A \ --allocation-id $EIP_ALLOC \ --query 'NatGateway.NatGatewayId' --output text)
# Wait for NAT Gateway to be availableaws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_ID
# Create private route table with NAT routePRIV_RTB=$(aws ec2 create-route-table \ --vpc-id $VPC_ID \ --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_IDaws ec2 associate-route-table \ --subnet-id $PRIV_SUBNET_1A \ --route-table-id $PRIV_RTBOne NAT Gateway Per AZ
A common mistake: creating one NAT Gateway in one AZ and pointing all private subnets across all AZs at it. If that AZ has a failure, all private subnet outbound internet traffic fails.
The correct pattern: one NAT Gateway per AZ, each private route table in a given AZ points to the NAT Gateway in the same AZ.
AZ us-east-1a: NAT-GW-1a in public-1a private-1a route: 0.0.0.0/0 → NAT-GW-1a
AZ us-east-1b: NAT-GW-1b in public-1b private-1b route: 0.0.0.0/0 → NAT-GW-1bThis is more expensive (two NAT Gateways vs one) but eliminates the cross-AZ failure dependency and also eliminates cross-AZ NAT data transfer charges.
Subnet Sizing Considerations
AWS reserves 5 IP addresses in each subnet:
.0— Network address.1— VPC router.2— DNS server (VPC +2 address).3— Reserved for future use.255— Broadcast address
A /24 subnet has 256 total IPs, 251 usable. For ECS with awsvpc networking, each task gets its own IP. 200 concurrent tasks need at least a /24. Plan accordingly.
For subnets that will host many ENIs (ECS Fargate, Lambda in VPC, EKS nodes with many pods), consider /22 (1,024 - 5 = 1,019 usable) or even /21 (2,048 - 5 = 2,043 usable) subnets.
Security Groups and Subnets
Security groups are attached to instances (or ENIs), not subnets. A subnet itself has no security group. The security controls at the subnet level come from NACLs.
A common three-tier security group pattern:
sg-alb: Inbound: 443 from 0.0.0.0/0 Outbound: 8080 to sg-app
sg-app: Inbound: 8080 from sg-alb Outbound: 5432 to sg-db Outbound: 443 to 0.0.0.0/0 (for external API calls via NAT)
sg-db: Inbound: 5432 from sg-app Outbound: (none needed for responses — stateful)The database security group only accepts connections from the application security group. Even if someone deployed an EC2 instance directly in the isolated subnet, it cannot reach the database unless its security group is sg-app.
Real-World Traffic Flow
A request from a user to your web application:
User browser │ ▼ HTTPS request (public internet)Internet Gateway │ ▼ Routes to ALB (public subnet 10.0.1.0/24)Application Load Balancer │ (checks sg-alb: 443 allowed inbound) │ ▼ Forwards to app instance on port 8080 (private subnet 10.0.11.0/24)EC2 / ECS Task │ (sg-app: allows 8080 from sg-alb) │ ▼ Database query on port 5432 (isolated subnet 10.0.21.0/24)RDS PostgreSQL │ (sg-db: allows 5432 from sg-app) │ ◄ Response flows back up the same pathThe database never sees a public IP, a public subnet, or a route to the internet.
Common Interview Questions
Q: What makes a subnet public?
Its associated route table has a route for 0.0.0.0/0 pointing to an Internet Gateway. Without that route, the subnet cannot route traffic to the internet regardless of other settings.
Q: Can a resource in a private subnet communicate with S3? Yes, through several paths: NAT Gateway (traffic goes out to the internet and back), a VPC Gateway Endpoint for S3 (private route within AWS, free), or a VPC Interface Endpoint. The Gateway Endpoint is the recommended approach — free and keeps traffic on the AWS network.
Q: Why would you put databases in an isolated subnet instead of a private subnet? An isolated subnet has no outbound internet route at all. Even if someone misconfigures the database instance to try to reach the internet, it cannot. A private subnet with a NAT Gateway route is slightly less restrictive. For PCI, HIPAA, or financial data, isolated subnets are the conservative and auditable choice.
Q: How many IPs can you use from a /28 subnet? A /28 has 16 total IPs. AWS reserves 5, leaving 11 usable. /28 is the minimum subnet size AWS allows.