VPC and Subnet Design Fundamentals
Concept
A Virtual Private Cloud (VPC) is a logically isolated network within a cloud provider's infrastructure — your own private slice of the network, with its own IP address range, that you carve into subnets and wire together with route tables and gateways. In AWS terms: a VPC spans an entire Region, but each subnet lives inside exactly one Availability Zone and cannot span AZs — this is what forces the "spread resources across subnets in multiple AZs" pattern for high availability, since a subnet itself is a single-AZ unit of failure isolation.
The VPC is assigned a CIDR block (e.g. 10.0.0.0/16) when created, and every subnet carved out of it gets its own smaller CIDR block from within that range (e.g. 10.0.1.0/24). Sizing this up front matters more than it looks: AWS reserves the first four addresses and the last address of every subnet for internal networking purposes (network address, VPC router, DNS, future use, and broadcast), so a /24 subnet's 256 addresses yield only 251 usable ones — and CIDR ranges are expensive to change later without downtime, since every route table, security group, and peering connection references them.
Every subnet must be associated with a route table that governs where its outbound traffic goes. A newly created subnet is automatically associated with the VPC's main route table; whether a subnet is "public" or "private" isn't a subnet property at all — it's entirely determined by what that route table points a 0.0.0.0/0 (all other traffic) route at (see Public vs. Private Subnets for that distinction in depth, and NAT Gateway vs. NAT Instance for how a private subnet still reaches the internet for outbound-only calls).
Tradeoffs
| Design choice | Benefit | Cost |
|---|---|---|
| One large VPC, many subnets | Simple mental model, easy cross-service routing within the VPC | Blast radius of a misconfiguration (e.g. an overly permissive route or NACL) covers everything in it; CIDR range is a single shared budget across every subnet |
| Many small VPCs (one per team/service) | Strong isolation, independent CIDR planning per team | Cross-VPC traffic needs peering or a Transit Gateway; uncoordinated CIDR allocation across VPCs risks overlapping ranges that block peering later (see Slack's Whitecastle case study for exactly this failure at scale) |
| Wide subnet CIDR (e.g. /20) | Room to grow — rarely re-carve subnets as workloads scale | Wastes address space if actual usage stays small, and a VPC's total address space is finite once chosen |
| Narrow subnet CIDR (e.g. /28) | Efficient use of a scarce VPC CIDR budget | Easy to exhaust — auto-scaling groups, Lambda ENIs, and NAT Gateways all consume subnet IPs, and running out mid-incident with no easy resize is a real failure mode |
The subnet-CIDR-size decision is the one with the least reversibility here: resizing later, without an outage, generally means creating new subnets and migrating resources rather than editing one in place.
When to use / when not to
- Every cloud resource that isn't purely regional/global (like S3 or most managed DNS) needs to live in a subnet, so this isn't optional — the actual decision is how many subnets, how they're sized, and which AZs they cover.
- Spread stateful and customer-facing workloads across at least two AZs' worth of subnets whenever the availability requirement is anything beyond "acceptable to lose the whole service if one data center has a bad day."
- Don't default every subnet's CIDR block to the smallest size that fits today's resource count — leave headroom for autoscaling and for services (NAT Gateways, load balancers, some managed services) that silently consume more IPs per instance than a single EC2 instance does.
- Don't allocate VPC CIDR ranges independently per team/account without a central plan once there's more than a handful of accounts — the coordination cost of fixing overlapping ranges later is far higher than planning a non-overlapping allocation scheme up front.
Common pitfall
Treating "public subnet" and "private subnet" as a checkbox set at subnet-creation time rather than a live property of the route table. A subnet with a 0.0.0.0/0 route to an Internet Gateway is public; remove or repoint that one route and it's private, with no other subnet-level setting involved. This matters operationally: an accidental route table change (or associating the wrong route table to a new subnet) can silently make a subnet that was meant to be private internet-reachable, or vice versa — the subnet's name in the console doesn't enforce anything.
Engineering Lens
VPC/subnet design is a capacity-planning problem wearing a networking costume: the CIDR block you choose on day one is a budget, and like any budget it's easiest to get right before anything depends on it and hardest to fix once dozens of route tables, security groups, and peering connections reference the ranges you picked. The Slack Whitecastle case study is the canonical lesson in what happens when that budget is allocated account-by-account with no central coordination — it works fine until the day two ranges need to talk to each other and can't. The transferable instinct: any resource that's cheap to size generously up front and expensive to resize later (CIDR blocks, primary keys, sharding schemes) deserves deliberate headroom, not the minimum that satisfies today's requirements.
Related
- Public vs. Private Subnets
- NAT Gateway vs. NAT Instance
- Slack: Whitecastle — Rebuilding Slack's Cloud Network to Escape the VPC Peering Trap
- IP Routing Tables and NAT