Skip to main content
AWS·21 min read

VPC Subnet Sizing — Decisions You Cannot Change Later Without Rebuilding Everything

Master VPC architecture, subnet sizing, CIDR planning, NAT strategy, and VPC Flow Logs. Includes 3 real-world case studies with Terraform-ready designs that handle auto-scaling without IP exhaustion.

DT

DevOps Engineer & Technical Writer

Why VPC Design is the Foundation That Makes or Breaks Everything

VPC ARCHITECTURE — 3 AZs WITH PUBLIC / PRIVATE / DATA SUBNETS VPC 10.0.0.0/16 INTERNET IGW AZ-a (us-east-1a) PUBLIC /24 ALB + NAT GW PRIVATE /19 EKS Pods / Compute ISOLATED /24 RDS / ElastiCache AZ-b (us-east-1b) PUBLIC /24 ALB + NAT GW PRIVATE /19 EKS Pods / Compute ISOLATED /24 RDS / ElastiCache AZ-c (us-east-1c) PUBLIC /24 ALB + NAT GW PRIVATE /19 EKS Pods / Compute ISOLATED /24 RDS / ElastiCache ROUTE TABLE 0.0.0.0/0 → NAT GW ROUTE TABLE 0.0.0.0/0 → NAT GW ROUTE TABLE 0.0.0.0/0 → NAT GW Public Private Isolated/Data

Every AWS resource lives inside a VPC. Get the design wrong and you face:

  • IP exhaustion during auto-scaling — pods cannot schedule, instances cannot launch
  • Costly re-architecture — you cannot resize a subnet; you must recreate everything
  • Security gaps — flat networks where a compromised workload reaches every database
  • Compliance failures — auditors asking why production and dev share network space

This guide gives you the knowledge to design VPCs that last years without re-architecture.

---

The 5 Critical Decisions in VPC Design

Decision 1: VPC CIDR Block Size

Your VPC CIDR defines the total IP pool. Once set, the primary CIDR cannot be changed.

Common CIDR blocks and their capacity:

CIDRTotal IPsUsable IPsBest For
/1665,53665,531Production VPCs, EKS clusters, large workloads
/204,0964,091Small/medium workloads, dev environments
/24256251Tiny VPCs, single-purpose networks

Rule of thumb: Always use /16 for production VPCs. IP addresses are free — running out of them is expensive. A /16 gives you 65,000+ addresses, enough for years of growth.

AWS reserves 5 IPs per subnet: first 4 and last 1 (network address, VPC router, DNS, reserved, broadcast). A /24 subnet gives 251 usable IPs, not 256.

Private IP ranges (RFC 1918):

  • 10.0.0.0/8 — 16.7M addresses (recommended for enterprise)
  • 172.16.0.0/12 — 1M addresses
  • 192.168.0.0/16 — 65K addresses (avoid — conflicts with home networks)

Critical rule: If you plan VPC peering or Transit Gateway, CIDRs cannot overlap. Plan non-overlapping addressing across all VPCs from day one.

Example enterprise scheme:

Production:  10.0.0.0/16

Staging: 10.1.0.0/16

Development: 10.2.0.0/16

Shared: 10.3.0.0/16

DR: 10.4.0.0/16

Decision 2: Subnet Tiers (Public, Private, Isolated)

A production VPC needs three tiers:

Public Subnets — Direct internet access:

  • Application Load Balancers (ALBs)
  • NAT Gateways
  • Bastion hosts (if used)
  • NOT application servers or databases

Private Subnets — Outbound internet via NAT, no inbound:

  • Application servers (EC2, ECS tasks, EKS pods)
  • Workers and background processors
  • Internal microservices

Isolated/Data Subnets — No internet access at all:

  • RDS databases
  • ElastiCache clusters
  • Internal-only services

This three-tier model ensures defense-in-depth.

Decision 3: Availability Zones

Always deploy across 3 AZs for production. With 3 tiers x 3 AZs = 9 subnets minimum.

Decision 4: Subnet Sizing (Where Most Teams Fail)

Subnet size determines how many resources can exist in that subnet. If EKS pods auto-scale and the subnet runs out of IPs, scaling fails silently — no new pods, no error in the application, just degraded performance until someone notices.

The EKS IP consumption problem:

Each EKS pod gets its own IP address (AWS VPC CNI). A single EC2 node running 30 pods consumes 30+ IPs from the subnet. A cluster with 10 nodes and 30 pods each = 300+ IPs just for pods. During a traffic spike that triggers HPA scaling to 100 pods per node across 20 nodes = 2,000 IPs consumed instantly.

If your private subnet is a /24 (251 usable IPs), you run out with just 8 nodes.

Recommended subnet sizes:

Subnet TypeRecommended SizeUsable IPsReasoning
Public/24251Only ALBs and NAT GWs — few IPs needed
Private (EKS/ECS)/198,187Pods consume IPs aggressively during scaling
Private (general compute)/204,091EC2 instances, moderate growth
Isolated (databases)/24251Few RDS instances, rarely scales

The /19 for EKS is critical. With 8,187 IPs per subnet across 3 AZs, you have 24,000+ IPs for pods — enough for hundreds of nodes and thousands of pods.

IP consumption formula for EKS:

IPs needed = (max_nodes × max_pods_per_node) + (max_nodes × ENI_overhead)

Example: 50 nodes × 50 pods + 50 × 3 ENIs = 2,650 IPs needed

With /19 (8,187 per AZ): plenty of headroom

With /24 (251 per AZ): FAILURE at 8 nodes

Decision 5: NAT Gateway Strategy

NAT Gateways allow private subnet resources to reach the internet (pull Docker images, call external APIs, download packages) without being directly accessible from the internet.

Options:

StrategyCostAvailabilityBest For
1 NAT GW in 1 AZ~$32/month + dataNo AZ redundancyDev/staging
1 NAT GW per AZ (3 total)~$96/month + dataFull AZ redundancyProduction
NAT Instance (t3.micro)~$8/monthLow throughputCost-sensitive dev

Production rule: One NAT Gateway per AZ. If a single-AZ NAT GW fails, all private subnet resources in other AZs lose internet access. At $32/month per NAT GW, the $96/month total is cheap insurance.

Data processing charges: NAT Gateway charges $0.045/GB processed. At 1TB/month egress through NAT, that is $45 in processing fees alone. For high-bandwidth workloads (pulling large Docker images, data transfers), consider VPC endpoints to bypass NAT for AWS service traffic.

---

VPC Flow Logs — Enhanced Logging for Security and Troubleshooting

VPC Flow Logs capture information about IP traffic going to and from network interfaces in your VPC. They are essential for security auditing, troubleshooting connectivity issues, and compliance.

What Flow Logs Capture

Each flow log record contains:

  • Source and destination IP addresses
  • Source and destination ports
  • Protocol (TCP/UDP/ICMP)
  • Number of packets and bytes
  • Start and end time
  • Action (ACCEPT or REJECT)
  • Log status

Enabling Flow Logs

# Create flow log for entire VPC (recommended)

aws ec2 create-flow-logs \

--resource-type VPC \

--resource-ids vpc-0abc123def456 \

--traffic-type ALL \

--log-destination-type cloud-watch-logs \

--log-group-name /vpc/flow-logs/production \

--deliver-logs-permission-arn arn:aws:iam::123456789:role/vpc-flow-logs-role

# Or send to S3 for cheaper long-term storage

aws ec2 create-flow-logs \

--resource-type VPC \

--resource-ids vpc-0abc123def456 \

--traffic-type ALL \

--log-destination-type s3 \

--log-destination arn:aws:s3:::my-flow-logs-bucket/production/

Enhanced Flow Logs (v5 format)

Enhanced flow logs add fields that standard logs miss:

FieldPurpose
vpc-idWhich VPC the traffic belongs to
subnet-idWhich subnet — critical for debugging routing
instance-idWhich EC2 instance
tcp-flagsSYN, ACK, FIN, RST — diagnose connection issues
pkt-src-addrOriginal source before NAT translation
flow-directionIngress or egress — understand traffic patterns
traffic-pathShows if traffic went through NAT GW, TGW, peering

Enable enhanced format:

aws ec2 create-flow-logs \

--resource-type VPC \

--resource-ids vpc-0abc123def456 \

--traffic-type ALL \

--log-destination-type s3 \

--log-destination arn:aws:s3:::flow-logs-bucket/ \

--log-format '${version} ${vpc-id} ${subnet-id} ${instance-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${flow-direction} ${traffic-path} ${tcp-flags}'

Flow Log Use Cases

Security auditing: Detect unexpected outbound connections (data exfiltration), port scanning attempts, rejected traffic patterns that indicate misconfigured security groups.

Troubleshooting: "Why can't service A talk to service B?" Flow logs show if traffic is being REJECTED and at which network interface — pinpoints security group or NACL issues.

Compliance: PCI-DSS and SOC2 require network traffic logging. Flow Logs satisfy this requirement.

Cost: CloudWatch Logs ingestion at $0.50/GB. High-traffic VPCs can generate 10-50 GB/day of flow logs. Use S3 destination ($0.023/GB storage) with Athena for querying to reduce costs.

---

Case Study 1: E-Commerce Platform (50 Microservices on EKS)

Business Context

  • E-commerce company processing 200K orders/day
  • 50 microservices running on EKS
  • Traffic spikes 10x during flash sales (Diwali, Black Friday)
  • Must auto-scale from 100 pods to 2,000 pods within 5 minutes
  • RDS PostgreSQL + ElastiCache Redis + OpenSearch

The Problem They Faced

Original VPC used /24 subnets for everything. During their first major sale:

  • Pods scaled from 100 to 400 → worked fine
  • Scaled from 400 to 800 → subnet ran out of IPs in AZ-a
  • New pods stuck in Pending state: "failed to allocate IP address"
  • 30% of traffic dropped for 45 minutes until manual intervention

The Correct Design

VPC CIDR: 10.0.0.0/16 (65,536 IPs)

Subnet Layout:

SubnetCIDRAZUsable IPsPurpose
public-a10.0.0.0/24us-east-1a251ALB, NAT GW
public-b10.0.1.0/24us-east-1b251ALB, NAT GW
public-c10.0.2.0/24us-east-1c251ALB, NAT GW
private-eks-a10.0.32.0/19us-east-1a8,187EKS pods
private-eks-b10.0.64.0/19us-east-1b8,187EKS pods
private-eks-c10.0.96.0/19us-east-1c8,187EKS pods
data-a10.0.200.0/24us-east-1a251RDS, ElastiCache
data-b10.0.201.0/24us-east-1b251RDS, ElastiCache
data-c10.0.202.0/24us-east-1c251RDS, ElastiCache

Total private subnet capacity for EKS: 24,561 IPs (3 x 8,187)

Why /19 for EKS subnets:

  • Peak scaling: 100 nodes x 50 pods = 5,000 IPs
  • With /19 per AZ: 8,187 IPs available per AZ
  • Utilization at peak: 5,000 / 24,561 = 20% (plenty of headroom)
  • Can grow 5x before any concern

Key design decisions:

  • EKS pods use AWS VPC CNI (each pod gets a VPC IP) — requires large subnets
  • NAT Gateway per AZ — ensures scaling in any AZ is not dependent on cross-AZ NAT
  • Data subnets have no route to internet — RDS cannot be reached from outside
  • VPC Flow Logs to S3 with 90-day retention for security auditing

Terraform Snippet

module "vpc" {

source = "terraform-aws-modules/vpc/aws"

version = "5.5.0"

name = "ecommerce-production"

cidr = "10.0.0.0/16"

azs = ["us-east-1a", "us-east-1b", "us-east-1c"]

public_subnets = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]

private_subnets = ["10.0.32.0/19", "10.0.64.0/19", "10.0.96.0/19"]

database_subnets = ["10.0.200.0/24", "10.0.201.0/24", "10.0.202.0/24"]

enable_nat_gateway = true

single_nat_gateway = false # One per AZ for HA

one_nat_gateway_per_az = true

enable_flow_log = true

create_flow_log_cloudwatch_iam_role = true

create_flow_log_cloudwatch_log_group = true

flow_log_traffic_type = "ALL"

tags = {

Environment = "production"

ManagedBy = "terraform"

}

}

---

Case Study 2: SaaS Platform on ECS Fargate (Multi-Tenant)

Business Context

  • B2B SaaS platform with 500 enterprise customers
  • 15 microservices running on ECS Fargate
  • Each customer tenant has isolated resources
  • Moderate traffic — 10K requests/minute steady, 30K during business hours
  • Must comply with SOC2 (network logging required)

Design Considerations

  • Fargate tasks consume 1 IP each (simpler than EKS — no pod networking multiplication)
  • Peak: 15 services x 10 tasks each x 3 AZs = 450 IPs maximum
  • Growth forecast: 3x in 2 years = 1,350 IPs
  • /22 subnets (1,019 IPs each) are sufficient — no need for /19

VPC Design

VPC CIDR: 10.1.0.0/16

SubnetCIDRAZUsable IPsPurpose
public-a10.1.0.0/24ap-south-1a251ALB, NAT GW
public-b10.1.1.0/24ap-south-1b251ALB, NAT GW
public-c10.1.2.0/24ap-south-1c251ALB, NAT GW
private-app-a10.1.16.0/22ap-south-1a1,019ECS Fargate tasks
private-app-b10.1.20.0/22ap-south-1b1,019ECS Fargate tasks
private-app-c10.1.24.0/22ap-south-1c1,019ECS Fargate tasks
data-a10.1.200.0/24ap-south-1a251RDS Aurora, Redis
data-b10.1.201.0/24ap-south-1b251RDS Aurora, Redis
data-c10.1.202.0/24ap-south-1c251RDS Aurora, Redis

Total private capacity: 3,057 IPs (3 x 1,019) — handles 3x growth comfortably.

Why /22 is Right Here (Not /19)

ECS Fargate = 1 IP per task. With 15 services averaging 10 tasks = 150 IPs steady state. Even at 3x peak = 450 IPs. A /22 (1,019 IPs per AZ) gives 6x headroom beyond peak. No need to waste /19 address space.

Security Architecture

Internet → ALB (public subnet, Security Group: allow 443)

ECS Tasks (private subnet, SG: allow from ALB SG only)

RDS Aurora (data subnet, SG: allow from ECS SG only, port 5432)

Network ACLs (stateless, subnet-level):

  • Public subnets: Allow inbound 443 (HTTPS), allow outbound all
  • Private subnets: Allow inbound from public subnet CIDR, allow outbound all
  • Data subnets: Allow inbound from private subnet CIDR on DB ports ONLY, deny all other inbound

VPC Endpoints (bypass NAT, save cost, improve security):

  • S3 Gateway Endpoint (free) — Fargate pulls images from ECR via S3
  • ECR API + ECR DKR Interface Endpoints — container image pulls stay within VPC
  • CloudWatch Logs Endpoint — log shipping without NAT
  • Secrets Manager Endpoint — secret retrieval without internet

These endpoints eliminate NAT Gateway data processing charges for AWS service traffic (saving hundreds/month at scale) and improve security by keeping traffic within the AWS network.

SOC2 Compliance: Flow Logs Configuration

resource "aws_flow_log" "vpc_flow_log" {

vpc_id = module.vpc.vpc_id

traffic_type = "ALL"

log_destination_type = "s3"

log_destination = aws_s3_bucket.flow_logs.arn

max_aggregation_interval = 60 # 1-minute granularity

destination_options {

file_format = "parquet" # 70% smaller than text, faster Athena queries

per_hour_partition = true # Partition by hour for efficient querying

}

tags = { Name = "saas-production-flow-logs" }

}

# Retention policy: 1 year for SOC2 compliance

resource "aws_s3_bucket_lifecycle_configuration" "flow_logs_lifecycle" {

bucket = aws_s3_bucket.flow_logs.id

rule {

id = "retain-1-year"

status = "Enabled"

transition {

days = 30

storage_class = "STANDARD_IA"

}

transition {

days = 90

storage_class = "GLACIER"

}

expiration {

days = 365

}

}

}

---

Case Study 3: Startup MVP Growing to Scale (Plan for the Future)

Business Context

  • Early-stage startup with 3 engineers
  • Currently 3 services on EC2 (MVP phase)
  • Planning migration to EKS within 12 months
  • Serves Indian market (ap-south-1)
  • Budget-conscious but does not want to re-architect later

The Challenge

Design a VPC today that works for EC2 now and EKS later without re-building.

The Design: Future-Proof from Day One

VPC CIDR: 10.2.0.0/16 (full /16 even though current needs are tiny)

SubnetCIDRAZUsable IPsCurrent UseFuture Use
public-a10.2.0.0/24ap-south-1a251ALBALB
public-b10.2.1.0/24ap-south-1b251(unused)ALB
public-c10.2.2.0/24ap-south-1c251(unused)ALB
private-a10.2.32.0/19ap-south-1a8,1873 EC2 instancesEKS pods
private-b10.2.64.0/19ap-south-1b8,187(unused)EKS pods
private-c10.2.96.0/19ap-south-1c8,187(unused)EKS pods
data-a10.2.200.0/24ap-south-1a251RDS (single-AZ)RDS Multi-AZ
data-b10.2.201.0/24ap-south-1b251(unused)RDS replica
data-c10.2.202.0/24ap-south-1c251(unused)ElastiCache

Why /19 subnets even though only 3 EC2 instances today:

  • /19 costs nothing extra — subnet size does not affect billing
  • When they migrate to EKS in 12 months, the subnets are ready
  • No re-architecture, no new subnets, no application downtime for network changes
  • The 3 EC2 instances use 3 IPs out of 8,187 — that is fine

Cost-Optimized NAT Strategy for Startups

Phase 1 (MVP — saving money):

  • Single NAT Gateway in AZ-a only ($32/month)
  • All private subnets route through the single NAT GW
  • Risk: If AZ-a fails, private subnets in AZ-b/c lose internet
  • Acceptable for a startup with low traffic and non-critical workloads

Phase 2 (Growing — when revenue justifies it):

  • Add NAT Gateways in AZ-b and AZ-c ($96/month total)
  • Update route tables so each AZ routes to its own NAT
  • Zero downtime change — just add routes

Phase 3 (Production — EKS migration):

  • Add VPC endpoints for ECR, S3, CloudWatch (reduces NAT data charges)
  • Enable VPC Flow Logs (compliance requirement for enterprise customers)
  • Implement Transit Gateway for multi-VPC connectivity (shared services VPC)

The Key Insight

Designing for /19 private subnets costs you nothing today but saves you from a painful re-architecture later. The only cost difference between a /24 and a /19 subnet is zero — you are just allocating address space from your /16 VPC. The mistake teams make is being conservative with subnet sizing because they think small subnets are "simpler." They are not — they are a ticking time bomb.

---

Common Mistakes (What Not To Do)

Mistake 1: Using /24 for EKS private subnets

Impact: Auto-scaling fails at ~8 nodes (50 pods each = 400 IPs > 251 available)

Fix: Use /19 minimum for any subnet running EKS pods

Mistake 2: Single NAT Gateway for production

Impact: AZ failure takes down all private subnet internet access

Fix: One NAT GW per AZ for production workloads

Mistake 3: No VPC Flow Logs

Impact: Cannot debug connectivity issues, fails security audits

Fix: Enable flow logs to S3 on day one (cost: $0.023/GB stored)

Mistake 4: Overlapping CIDRs across environments

Impact: Cannot peer VPCs, cannot use Transit Gateway, cannot connect to on-premises

Fix: Plan non-overlapping /16 blocks per environment from day one

Mistake 5: Putting databases in private subnets (not isolated)

Impact: Compromised application server can route to internet and exfiltrate data

Fix: Use isolated subnets with no internet route for databases

Mistake 6: Not planning for VPC endpoints

Impact: All AWS service traffic routes through NAT GW, costing $0.045/GB

Fix: Add S3, ECR, CloudWatch, Secrets Manager endpoints to avoid NAT for AWS traffic

---

Quick Reference: Subnet Sizing Decision Matrix

Workload TypeMinimum Subnet SizeRecommendedWhy
ALB only/28 (11 IPs)/24Room for future ALBs
EC2 instances (< 20)/24 (251 IPs)/24Adequate for small fleets
ECS Fargate (< 100 tasks)/22 (1,019 IPs)/221 IP per task, moderate scaling
ECS Fargate (100-500 tasks)/20 (4,091 IPs)/20High-density task scheduling
EKS pods (any scale)/19 (8,187 IPs)/19Pods consume IPs aggressively
RDS / ElastiCache/24 (251 IPs)/24Few instances, rarely scales
Lambda (VPC-attached)/19 (8,187 IPs)/19Each concurrent execution uses an ENI IP

Important: VPC-attached Lambda functions consume IPs from your subnet for each concurrent execution. At 1,000 concurrent Lambda invocations = 1,000 IPs needed. If your Lambda is in a /24 subnet, it fails at 251 concurrent executions.

---

Summary

  • Always use /16 for the VPC — IP addresses are free, re-architecture is not
  • Use /19 for private subnets running EKS, Lambda, or any auto-scaling workload
  • Three tiers minimum — public (ALB/NAT), private (compute), isolated (data)
  • Three AZs minimum — single AZ is not production-ready
  • One NAT Gateway per AZ for production
  • Enable VPC Flow Logs from day one — you will need them eventually
  • Add VPC Endpoints for S3, ECR, CloudWatch to save NAT costs
  • Plan non-overlapping CIDRs across all VPCs for future peering
  • ---

    Frequently Asked Questions

    How do I choose the right VPC CIDR block?

    Use a /16 CIDR (65,536 IPs) for production VPCs to allow growth, ensuring it doesn't overlap with other VPCs, on-premises networks, or partner networks you might peer with. Common choices are 10.0.0.0/16 for production, 10.1.0.0/16 for staging. Plan your IP space across all environments upfront — CIDR conflicts prevent VPC peering and transit gateway attachments later.

    What is the difference between public and private subnets?

    Public subnets have a route to an Internet Gateway, giving resources public IP addresses and direct internet access. Private subnets route internet traffic through a NAT Gateway — resources have only private IPs and can reach the internet but aren't directly reachable from it. Place databases, application servers, and internal services in private subnets; only load balancers and bastion hosts need public subnets.

    How many subnets do I need per VPC?

    At minimum, create one public and one private subnet per availability zone — typically 6 subnets for a 3-AZ deployment. For better isolation, add a separate data subnet tier for databases. This gives you 9 subnets (3 public + 3 private/app + 3 private/data). Each subnet should be at least /24 (254 IPs) to accommodate scaling.

    How do I calculate subnet sizes using CIDR notation?

    A /24 gives 256 IPs (251 usable after AWS reserves 5), /25 gives 128, /26 gives 64, /27 gives 32. Divide your VPC CIDR evenly: a /16 VPC can hold 256 /24 subnets. For quick mental math, each bit in the subnet mask doubles/halves the range. Use /24 for most application subnets and /28 (16 IPs) for small subnets like NAT Gateway placements.

    What is a NAT Gateway and when do I need one?

    A NAT Gateway allows resources in private subnets to initiate outbound internet connections (for updates, API calls) while remaining unreachable from the internet. You need one in each AZ where private subnets have resources needing internet access. NAT Gateways cost $0.045/hour plus $0.045/GB processed — for cost savings in development, use a single NAT Gateway or NAT instance instead.

    ---