Skip to content
AWS aws networking 5 min read

Subnets: Public vs Private

A Virtual Private Cloud (VPC) gives you one big private network in the cloud, but you almost never put everything in a single flat space. Instead you carve the VPC into subnets — smaller slices of its address range. Subnets let you place internet-facing things (like load balancers) in one place and keep internal things (like databases) tucked away where the internet cannot reach them. The surprising part, which this page makes crystal clear, is that “public” and “private” are not real settings — they are just descriptions of how a subnet is wired.

What a subnet actually is

A subnet is two things bolted together:

  1. A CIDR block — a slice of your VPC’s IP address range. CIDR (Classless Inter-Domain Routing) is the notation like 10.0.1.0/24 that describes a range of IP addresses. If your VPC is 10.0.0.0/16 (65,536 addresses), a /24 subnet carves out 256 of them.
  2. An Availability Zone (AZ) — a physically separate data center within an AWS Region. A subnet lives in exactly one AZ and cannot span more than one. This is the key fact that drives high availability (HA): to survive an AZ failure, you spread your subnets across multiple AZs.

Note: AWS reserves 5 IP addresses in every subnet (the first four and the last one) for networking, DNS, and future use. So a /24 subnet gives you 251 usable addresses, not 256.

What makes a subnet “public” vs “private”

This is the single most misunderstood idea in VPC networking, so read it slowly. There is no checkbox that marks a subnet as public or private. A subnet is “public” only when both of these are true:

  • Its route table has a route sending 0.0.0.0/0 (all internet traffic) to an internet gateway (IGW) — the VPC component that allows traffic to and from the public internet.
  • The instances inside it have a public IP address (or an Elastic IP, which is a permanent public IP address you own).

A “private” subnet is simply one whose route table does not point 0.0.0.0/0 at an internet gateway. Resources there can still reach the internet for outbound updates if you add a NAT gateway (a managed device that lets private resources make outbound connections without being reachable from outside), but nothing on the internet can start a connection to them.

Gotcha: The names “public-subnet” and “private-subnet” are just a naming convention engineers use. The routing is what actually matters. If you name a subnet “private” but its route table points at an internet gateway and your instance has a public IP, it is fully exposed to the internet. Always verify the route table — never trust the label.

PropertyPublic subnetPrivate subnet
Route to 0.0.0.0/0Internet gatewayNAT gateway or none
Inbound from internetYes (if public IP)No
Outbound to internetDirectVia NAT gateway only
Typical residentsLoad balancers, bastion hostsDatabases, app servers

When to use which

  • Public subnets — only for resources that genuinely must accept connections from the internet, such as an Application Load Balancer or a bastion host (a hardened jump server). Keep these minimal.
  • Private subnets — the default home for everything else: application servers, databases, caches, internal queues. If a resource does not need to be reached from the internet, it belongs here.

A common, recommended layout is one public and one private subnet per AZ, across at least two AZs.

Creating subnets

Console steps

  1. Open the VPC console and choose Subnets in the left menu.
  2. Click Create subnet.
  3. Select your VPC (e.g. vpc-0a1b2c3d).
  4. Enter a Subnet name (e.g. public-1a), pick an Availability Zone (e.g. us-east-1a), and set the IPv4 CIDR block (e.g. 10.0.1.0/24).
  5. Click Add new subnet to repeat for another AZ, then Create subnet.
  6. To make a subnet public: select it, open the Route table tab, and edit its associated route table so that 0.0.0.0/0 targets your internet gateway.

AWS CLI

# Create a public subnet in us-east-1a
aws ec2 create-subnet \
  --vpc-id vpc-0a1b2c3d \
  --cidr-block 10.0.1.0/24 \
  --availability-zone us-east-1a \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-1a}]'

Output:

{
    "Subnet": {
        "AvailabilityZone": "us-east-1a",
        "CidrBlock": "10.0.1.0/24",
        "State": "available",
        "SubnetId": "subnet-0a1b2c3d",
        "VpcId": "vpc-0a1b2c3d",
        "MapPublicIpOnLaunch": false
    }
}

Notice MapPublicIpOnLaunch is false. To have instances launched here automatically get a public IP, turn it on:

aws ec2 modify-subnet-attribute \
  --subnet-id subnet-0a1b2c3d \
  --map-public-ip-on-launch

The route to the internet gateway is what truly makes it public:

aws ec2 create-route \
  --route-table-id rtb-0a1b2c3d \
  --destination-cidr-block 0.0.0.0/0 \
  --gateway-id igw-0a1b2c3d

Spreading across AZs for high availability

Because a subnet is locked to one AZ, a single subnet is a single point of failure. The fix is to create matching subnets in multiple AZs and place your resources in all of them.

# Terraform: two public + two private subnets across two AZs
variable "azs" { default = ["us-east-1a", "us-east-1b"] }

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = "vpc-0a1b2c3d"
  cidr_block        = cidrsubnet("10.0.0.0/16", 8, count.index)       # 10.0.0.0/24, 10.0.1.0/24
  availability_zone = var.azs[count.index]
  tags = { Name = "public-${var.azs[count.index]}" }
}

resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = "vpc-0a1b2c3d"
  cidr_block        = cidrsubnet("10.0.0.0/16", 8, count.index + 10)  # 10.0.10.0/24, 10.0.11.0/24
  availability_zone = var.azs[count.index]
  tags = { Name = "private-${var.azs[count.index]}" }
}

With this layout, an Application Load Balancer can span both public subnets and route to app servers spread across both private subnets — so losing one AZ does not take down your service.

Cost note: Subnets themselves are free. The cost comes from what you attach to them. A NAT gateway (needed for outbound internet from private subnets) costs roughly $0.045 per hour (~$32/month) per gateway plus a per-GB data processing charge in us-east-1. For HA you typically run one NAT gateway per AZ, so two AZs means roughly $64/month before data charges.

Best practices

  • Plan non-overlapping CIDR blocks up front; you cannot resize a subnet’s primary range after creation.
  • Use at least two AZs (three for critical workloads) and create one public and one private subnet in each.
  • Keep public subnets small — put only load balancers and bastion hosts there, never databases.
  • Always confirm “public/private” by inspecting the route table, not the subnet name.
  • Disable auto-assign public IP on private subnets to avoid accidental exposure.
  • Tag subnets clearly (tier, AZ, environment) so automation and humans can tell them apart.
  • Reserve spare address space so you can add subnets later without re-architecting.
Last updated June 15, 2026
Was this helpful?