VPC and subnets: CIDR, routing, and the NAT egress trap
A VPC is an isolated virtual network you define by CIDR; subnets carve it per-AZ, and "public" vs "private" is decided by routing, not a flag. Master IGW, NAT, and gateway endpoints — or eat a five-figure egress bill.
A data team runs a nightly Spark job in a private subnet that reads and writes terabytes to S3. It works flawlessly for a year. Then finance forwards the AWS bill with one line circled in red: tens of thousands of dollars under “NAT Gateway — data processing.” Nobody changed the code. What happened is that every byte to S3 had been routed through a NAT Gateway the whole time, and NAT charges a per-GB data-processing fee on top of its hourly rate. The traffic never needed NAT at all — S3 has a free VPC Gateway Endpoint that keeps it off NAT entirely. One route-table entry, added in five minutes, erased the line item. The lesson is brutal and common: in a VPC, where your packets go is decided by routing you configure, and the wrong default route is a silent invoice.
CIDR, subnets, and the per-AZ rule
By the end of this section you will know exactly which two things make a subnet “public,” and why the wrong answer costs money every hour.
A VPC (Virtual Private Cloud) is a logically isolated virtual network you carve out inside an AWS Region. You define its address space with a CIDR block, for example 10.0.0.0/16 — that gives you 65,536 private IPv4 addresses to hand out. Nothing inside one VPC can reach another VPC or the internet unless you explicitly wire a path; isolation is the default.
You don’t put resources directly in a VPC — you put them in subnets, which carve the VPC CIDR into smaller ranges like 10.0.1.0/24. The single most important structural fact: a subnet lives entirely in one Availability Zone and cannot span AZs. An AZ is an isolated datacenter cluster, so if you place every subnet in one AZ and that AZ has an outage, your whole tier goes dark. High availability is therefore non-negotiable: spread subnets across at least two AZs and run a copy of each tier in each.
VPC CIDR 10.0.0.0/16 (65,536 IPs, one Region)
AZ us-east-1a
public 10.0.0.0/24 web / load balancer / NAT
private 10.0.1.0/24 app servers, databases
AZ us-east-1b
public 10.0.2.0/24
private 10.0.3.0/24A subnet’s CIDR must be a subset of the VPC CIDR, and the two cannot overlap each other. AWS also reserves the first four and last IP in every subnet, so a /24 gives you 251 usable addresses, not 256 — a detail that bites when you size subnets too tight for an autoscaling fleet. When you plan your CIDR layout, always leave headroom: a /24 looks generous until you have 200 Lambda ENIs or an EKS node pool expanding under load.
”Public” and “private” are routing, not a flag
There is no checkbox that makes a subnet public. A subnet is public only if two things are true: its associated route table sends 0.0.0.0/0 (all internet-bound traffic) to an Internet Gateway (IGW), and the instances in it have public IP addresses. Remove either and the same subnet is effectively private. The IGW is a horizontally-scaled, redundant VPC component that provides bidirectional internet connectivity; AWS does not charge an hourly fee for the gateway itself (you pay normal data-transfer rates).
Every subnet is associated with exactly one route table, a list of destination → target rules evaluated by longest-prefix match. Every route table has an immutable local route for the VPC CIDR, which is what lets any subnet talk to any other subnet inside the VPC with no extra configuration.
# Public subnet route table
Destination Target
10.0.0.0/16 local # intra-VPC, always present
0.0.0.0/0 igw-0abc123 # internet, both directions
# Private subnet route table
Destination Target
10.0.0.0/16 local
0.0.0.0/0 nat-0def456 # egress only, via NAT in a public subnetA NAT Gateway is how private instances reach out to the internet — for OS updates, package registries, third-party APIs — without being reachable from the internet. Per AWS docs, instances in a private subnet connect outbound through a NAT Gateway, but external services cannot initiate a connection back. A public NAT Gateway must live in a public subnet and have an Elastic IP, and its route ultimately sends traffic to the IGW. It is a managed, zonal resource: it exists in one AZ.
The egress-cost trap and how endpoints kill it
Here is the trap that produces five-figure surprises. A NAT Gateway bills on two axes: an hourly charge for each gateway, plus a per-GB data-processing charge on every byte that flows through it (prices are illustrative and region-dependent — see the Amazon VPC pricing page for current rates). When a private instance talks to S3, DynamoDB, or another high-volume service through the default 0.0.0.0/0 to NAT route, every gigabyte is taxed by that data-processing fee — and at terabyte scale it dwarfs the actual compute cost.
The fix is a VPC Gateway Endpoint. Per AWS docs, gateway endpoints provide connectivity to S3 and DynamoDB “without requiring an internet gateway or a NAT device,” and there is no additional charge for using them. You enable one by selecting your private subnets’ route tables; AWS auto-adds a route whose destination is an AWS-managed prefix list for the service and whose target is the endpoint. Because routing uses longest-prefix match, S3-bound traffic now takes the free endpoint route instead of the NAT default — and stays entirely on the AWS network.
For services without a gateway endpoint, Interface Endpoints (PrivateLink) put an ENI in your subnet so traffic to that service stays off the public internet (these carry their own hourly + per-GB charge, but avoid NAT data-processing and the internet path).
| Path component | Direction | Cost shape | HA concern |
|---|---|---|---|
| Internet Gateway | Inbound + outbound | No hourly gateway fee; data transfer applies | Region-wide, redundant |
| NAT Gateway | Outbound only | Hourly + per-GB data processing | Zonal — one per AZ for HA |
| Gateway Endpoint (S3/DynamoDB) | To service only | No additional charge | Per route table, no ENI |
| Interface Endpoint (PrivateLink) | To service only | Hourly + per-GB (no NAT fee) | One ENI per AZ for HA |
▸Why this works
Why does NAT have a separate per-AZ failure story? A NAT Gateway is zonal — it lives in exactly one AZ. If you build one NAT Gateway in us-east-1a and point the private route tables of every AZ at it, you get two failure modes at once. First, if us-east-1a fails, instances in 1b and 1c lose all egress even though their own AZ is healthy — the single NAT is an AZ-scoped single point of failure. Second, in normal operation, traffic from 1b/1c instances crosses AZ boundaries to reach that NAT, and cross-AZ traffic is itself billed per GB. The fix is one NAT Gateway per AZ, with each AZ’s private route table pointing at the NAT in its own AZ: it removes the SPOF and the cross-AZ data charge in a single move.
Private app servers across three AZs read and write 40 TB/month to S3 and also call a handful of third-party HTTPS APIs. You need egress that is highly available and not financially radioactive. Pick the primary design.
What actually makes a subnet 'public' in a VPC?
Private instances move 30 TB/month to S3 and the NAT Gateway data-processing line is enormous. Cheapest correct fix?
- 01What defines a public vs a private subnet, and how do private subnets reach the internet?
- 02Explain the NAT egress-cost trap and how VPC endpoints fix it, plus the NAT high-availability rule.
A VPC is a logically isolated virtual network you define by a CIDR block such as 10.0.0.0/16, and you place resources in subnets that carve that CIDR into smaller ranges. The structural rule that drives every availability decision is that a subnet lives entirely in one Availability Zone and cannot span AZs, so you spread subnets across at least two AZs to survive a zone outage. Public versus private is not a flag but a consequence of routing: a subnet is public only when its route table sends 0.0.0.0/0 to an Internet Gateway and its instances have public IPs, while a private subnet routes outbound through a zonal NAT Gateway sitting in a public subnet, which lets instances initiate connections out but blocks unsolicited inbound. The NAT Gateway is also where money quietly burns, because it charges per hour and per GB of data processed, so routing high-volume S3 or DynamoDB traffic through it produces five-figure surprises that a free VPC Gateway Endpoint eliminates by sending that traffic over a prefix-list route that never touches NAT and stays on the AWS network. Finish the picture with the high-availability rule — one NAT Gateway per AZ, each AZ’s route table pointing at its own NAT — to remove both the single-point-of-failure and the cross-AZ data charge, and you have a VPC whose paths, and whose bill, are chosen on purpose. Now when you open the AWS Cost Explorer and see an unexpected NAT Gateway line, your first question is: which route table is sending that traffic through NAT instead of through a free endpoint?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.