Cloud Infrastructure · Cloud Networking
VPC Address Space, Subnets, and the Implicit Router
How a CIDR block becomes an isolated forwarding domain, where the router joining its subnets actually runs, and why several addresses in every subnet are never usable.
A physical network gives each machine an address that the surrounding routers can reach because a cable, a switch port, or a leased line physically leads there. A virtual private cloud keeps the addressing model and discards the physical part. The addresses are chosen by the tenant, frequently out of the RFC 1918 private ranges, and the same numbers appear in thousands of unrelated networks running on the same hardware. What makes that work is not a new protocol but a change in where the forwarding decision is made and what it is made against.
A CIDR block is a forwarding domain, not a wire
A VPC begins as one or more IPv4 or IPv6 prefixes. The prefix is not a link: nothing is attached to it, and no host owns it as a whole. It is a declaration that addresses inside the range belong to this tenant, and that packets with those destinations are the tenant’s problem rather than the provider’s. Overlap between two VPCs is permitted and common, because the provider keys every lookup on an internal identifier for the network in addition to the address. Two VPCs can both use 10.0.0.0/16 without collision as long as nothing ever needs to route between them.
Within the block, the tenant carves subnets. A subnet is a contiguous slice of the VPC prefix with a single prefix length, and it is the unit that carries three separate meanings at once: an address range, a routing table attachment, and a placement constraint. That triple role is the source of most of the confusion in the model. A subnet is not merely an addressing convenience the way it often is on a physical network; it is the object to which policy is bound.
Prefix length determines capacity in the ordinary way. A /16 holds 65,536 addresses; splitting it into /24s yields 256 subnets; a /24 holds 256 addresses. The arithmetic is the same as anywhere else in CIDR, and the provider imposes bounds at both ends: a VPC prefix cannot be arbitrarily large, and a subnet cannot be smaller than a documented minimum. The bounds exist because the address space is allocated from a finite pool and because the per-subnet metadata structures are sized for realistic counts.
Subnets are pinned to a zone
The placement constraint is the one that shapes architecture most. Each subnet is created in exactly one availability zone, meaning one failure domain with its own power and cooling. A subnet never spans zones, so a zone outage removes every address in the subnets placed there, even though the VPC itself and its other subnets keep working.
The consequence is duplication. A workload that must survive a zone loss needs a subnet in each zone it intends to use, and its instances need addresses from the matching subnet. A three-tier application in three zones therefore has at least nine subnets, and the address plan has to reserve the same amount of space in each zone rather than spreading one contiguous range across them. Planning that reserves a /20 per zone per tier inside a /16 VPC consumes the block quickly: sixteen /20s fit in a /16, so three zones times three tiers already uses more than half.
IPv6 changes the arithmetic and not the structure. A provider commonly allocates a /56 to a VPC and a /64 to each subnet, which is far more space than any single subnet can use, since a /64 is the size IPv6 expects for a link. The subnet remains a zone-scoped object with its own route table; only the numbers are larger.
Where the router that joins the subnets runs
Subnets in different zones must reach each other, and the VPC prefix must be reachable from every subnet in it. That connectivity is provided by a router that exists as a logical function rather than as a device. There is no appliance sitting between two subnets, and no interface on which to observe traffic crossing from one to the other. The forwarding decision is taken at the source, in the virtual switch that owns the sending interface.
That placement is the central mechanism of the whole model. When an interface emits a packet, the virtual switch consults the route table attached to the source subnet, determines that the destination lies inside the VPC prefix, and resolves the destination’s location from the provider’s mapping of address to host. The packet is then carried across the provider’s physical fabric, often encapsulated, and delivered to the destination host’s virtual switch, which hands it to the right interface. The tenant’s “router” is really a distributed lookup performed twice, once at each end, with the physical network in between treated as a delivery service.
Because the lookup happens at the first hop, route changes take effect at the sender. A destination that moves does not require anything to be reconfigured along a path, because there is no path in the traditional sense: only the mapping from address to host, which the fabric maintains. It also means that packets already in flight during a change can be delivered according to the old mapping, and that two hosts sending to the same destination at slightly different times can observe different behaviour for a short window. Route propagation in a VPC is eventually consistent, not atomic.
Each subnet carries its own route table, and every table contains an entry for the VPC prefix itself. That entry is what makes intra-VPC traffic work, and it has priority over anything the tenant adds for the same prefix: it cannot be deleted or redirected. Traffic to an address inside the VPC is delivered by the fabric, not handed to a middlebox. Sending such traffic through an inspection appliance requires a more specific prefix that points at the appliance, which is why inspection designs insert routes for individual subnets rather than for the whole block. The details of how competing entries are ordered are covered in route table precedence and the local route.
The addresses that are never usable
The nominal size of a subnet is not the number of addresses a tenant can assign. In IPv4 the first address of a prefix is the network address and the last is the broadcast address, both unusable for hosts, which already removes two. Providers typically reserve more, because the subnet needs a gateway function and a resolver. A common allocation reserves the first four addresses and the last one, giving a /24 251 usable addresses rather than 254, and a /28 just eleven rather than fourteen. Small subnets suffer disproportionately: the overhead is constant while the capacity shrinks geometrically, so a /28 is a poor choice for anything but a dedicated appliance.
The reserved count is documented per provider and is worth checking before sizing, because it is not derivable from the prefix arithmetic. A plan that assumes 254 usable addresses in a /24 and places 254 addresses will fail to allocate the last three.
import ipaddress
vpc = ipaddress.ip_network("10.20.0.0/16")
# One /20 per (zone, tier) pair; a /16 splits into exactly 16 of them.
blocks = list(vpc.subnets(new_prefix=20))
assert len(blocks) == 16
RESERVED = 5 # network, gateway, resolver, and two held back
for zone in ("a", "b", "c"):
for tier in ("public", "app", "data"):
block = blocks.pop(0)
usable = block.num_addresses - RESERVED
print(f"{zone}/{tier:<6} {block} usable={usable}")
The script also shows the planning failure directly: once the list is exhausted, the next subnet has nowhere to come from, and the fix is a second, non-contiguous prefix added to the same VPC rather than a larger original block.
Filtering attaches to interfaces, not to segments
Security policy in a VPC is applied at the boundary of each interface, not on a shared segment. A packet from one instance to another in the same subnet still passes through two virtual switches and is evaluated at both ends. Nothing is shared that could be snooped: there is no hub, and the fabric does not forward a frame to interfaces that were not addressed.
Two filtering mechanisms sit at different levels. One is stateful and attached to an interface, tracking connections so that return traffic is permitted without an explicit rule. The other is stateless and attached to a subnet, evaluated in both directions with no memory of what was sent. Because the stateless variant has no connection state, a rule set that permits outbound requests but not the inbound replies silently drops the answers. The stateful mechanism and the connection table behind it are the subject of security groups and the stateful connection tracker.
What breaks first
The failure modes follow from the three roles of a subnet. Because a subnet is zone-bound, a zone loss is also an address-space loss, and an architecture that placed all its capacity in one zone loses all of it regardless of how many instances it ran. Because the address space is chosen by the tenant, two VPCs that later need to connect may be unable to do so at all if their prefixes overlap, which turns the original CIDR choice into a permanent constraint rather than a preference. Because the implicit route for the VPC prefix cannot be redirected, attempts to force intra-VPC traffic through an appliance by editing the route table for the whole block have no effect.
The last of these is worth stating as a general property rather than a provider quirk. A system that resolves destinations at the first hop has no place to put a chokepoint, so any design that needs one must create it by narrowing the destination prefix until the fabric no longer recognises the address as internal. That is the same trick used for private endpoints and for inspection architectures, and it is the reason address planning inside a VPC is really a plan for where traffic can be intercepted.
References
- RFC 1918: Address Allocation for Private Internets — IETF
- RFC 4632: Classless Inter-domain Routing (CIDR) — IETF
- RFC 1812: Requirements for IP Version 4 Routers — IETF
- Amazon Virtual Private Cloud User Guide — Amazon Web Services