CIDR, the netmask and the router's longest match
Your team carves a VPC out of 10.0.0.0/16, builds on it and ships. A year later the company buys another company. Their VPC? Also 10.0.0.0/16. Both networks are fine on their own. Together they’re an expensive lesson in why two machines can’t both be 10.0.0.5.
CIDR feels like trivia right up until that day. So: what the slash means, how a router picks between overlapping routes, how to get the network and broadcast address without a calculator, why /31 and /32 don’t follow the usual rule, and the planning mistakes that take an hour to make and years to undo.
A slash is a number of bits
An IPv4 address is 32 bits. Dotted-quad is just a readable way to write four bytes in decimal: 192.168.10.37 is the integer 0xC0A80A25 and nothing else.
CIDR notation (RFC 4632 [1]) adds a prefix length. 192.168.10.0/24 says the first 24 bits are fixed and the last 8 are free. The netmask says the same thing as a bit pattern, 24 ones and then 8 zeros, which in dotted form is 255.255.255.0.
Which means a valid netmask is always a run of ones followed by a run of zeros. 255.0.255.0 isn’t a netmask at all. The prefix length already says everything; the dotted mask is redundant.
A block holds 2^(32 - n) addresses. A /24 has 256, a /20 4096, a /30 four. Each step of one in the prefix doubles or halves the block.
To check whether dst is inside net/n, a host or router does one AND: dst & mask, then compare with net. That’s the whole test.
import ipaddress
net = ipaddress.ip_network("192.168.10.0/24")
print(net.netmask) # 255.255.255.0
print(net.num_addresses) # 256
print(ipaddress.ip_address("192.168.10.37") in net) # TrueDoing it in your head
Which block is 172.16.37.200/21 in?
A /21 leaves 11 host bits. 21 = 8 + 8 + 5, so the prefix ends inside the third octet, with 5 network bits and 3 host bits there. Block size in that octet: 2^3 = 8. The third octet is 37, and the largest multiple of 8 at or below 37 is 32.
So the network is 172.16.32.0. The broadcast is the last address in the block, 172.16.39.255 (third octet 32 + 8 - 1, fourth octet all ones). That leaves 2^11 - 2 = 2046 usable hosts, 172.16.32.1 through 172.16.39.254.
This works for any prefix. Find the octet where the prefix ends. The block size there is 2^(host bits in that octet). Round the address down to a multiple of it; the broadcast is the next multiple minus one.
| Prefix | Mask | Addresses | Usable hosts (classic) |
|---|---|---|---|
| /24 | 255.255.255.0 | 256 | 254 |
| /26 | 255.255.255.192 | 64 | 62 |
| /28 | 255.255.255.240 | 16 | 14 |
| /30 | 255.255.255.252 | 4 | 2 |
| /31 | 255.255.255.254 | 2 | 2 (see below) |
| /32 | 255.255.255.255 | 1 | 1 (a single host) |
The classic rule takes two off. The all-zeros host part is the network address, the name of the subnet. The all-ones host part is the directed broadcast. You can’t give either one to an ordinary host.
Do ten of these on paper with random addresses and prefixes, then check yourself in the CIDR calculator. After ten the octet trick sticks.
Longest prefix wins
A routing table is a list of prefix -> next hop entries, and they overlap on purpose. One rule makes that work: of all entries whose prefix contains the destination, use the one with the longest prefix.
0.0.0.0/0 -> upstream (default route)
10.0.0.0/8 -> core-router
10.20.0.0/16 -> site-a
10.20.30.0/24 -> lab-switch
10.20.30.7/32 -> special-box
10.20.30.99 matches the default, the /8, the /16 and the /24. The /24 wins. 10.20.30.7 matches all five and goes to special-box. 10.99.0.1 matches only the default and the /8, so it goes to the core.


Order doesn’t matter here. This isn’t a firewall rule list where the first match wins; the table is a set and specificity decides. The default route is simply /0: it matches everything and loses to everything, which is exactly what a fallback should do.
The uncomfortable side: whoever can inject a more specific route captures that traffic. Announce a /24 inside someone else’s /22 and the internet prefers you. That’s how BGP prefix hijacks work, and why operators filter on the maximum prefix length they’ll accept.
Routers do this in hardware with tries or TCAM, not a linear scan, but the result is the same as this loop:
import ipaddress
routes = {
"0.0.0.0/0": "upstream",
"10.0.0.0/8": "core",
"10.20.0.0/16": "site-a",
"10.20.30.0/24": "lab",
}
def lookup(dst):
ip = ipaddress.ip_address(dst)
best = max(
(ipaddress.ip_network(p) for p in routes if ip in ipaddress.ip_network(p)),
key=lambda n: n.prefixlen,
)
return routes[str(best)]
print(lookup("10.20.30.99")) # lab
print(lookup("10.99.0.1")) # core/31 and /32 are weird
The minus-two rule breaks down at the small end.
A /32 is one address. All 32 bits are network bits, there’s no host part to subtract anything from. It’s not a subnet with zero hosts; it’s a host route. You’ll see it on loopbacks, router IDs, firewall rules aimed at exactly one machine, and the special-box line in the table above.
A /31 has two addresses, so by the classic rule it would have zero usable hosts. That’s why point-to-point links traditionally got a /30: four addresses, two usable. RFC 3021 [2] changed that. A point-to-point link has nobody to broadcast to, so both addresses of a /31 can be hosts. If you have thousands of router-to-router links, that halves the address space they use.
But only on point-to-point links. On a shared Ethernet segment with many hosts, a /31 isn’t two ordinary hosts. And check your hardware and your cloud: some platforms reserve addresses no matter the prefix length. AWS keeps the first four and the last address of every subnet for itself, which is why its smallest subnet is a /28. “Usable hosts = 2^n - 2” is a textbook number. Find out what your platform actually reserves before you size a subnet to the exact host count.
Mistakes you find two years later
Most addressing mistakes are made in the first hour of a project and found much later.
Overlapping ranges are the big one. Networks that need to talk can’t share addresses, and “need to talk” keeps changing: VPNs, peering, an acquisition, a partner, a developer’s home LAN on 192.168.1.0/24 dialling into an office on the same range. Longest-prefix match can’t help when both sides have the identical prefix; the router can’t know which side an address belongs to. RFC 1918 [3] gives you 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16, and everyone grabs 10.0.0.0/16 or 192.168.0.0/24 first. Pick something less popular, and write down what you’ve allocated, even if it’s a spreadsheet.
Then misalignment. A block has to start on a multiple of its own size. 10.1.1.0/23 isn’t valid: a /23 covers two /24s and has to start at an even third octet, so the block containing that address is 10.1.0.0/23. Hand out odd-aligned ranges and you can’t summarise them into one route, so routing tables grow.
Sizing is the third. Giving every team a /16 because it’s a round number feels generous until you notice 10.0.0.0/8 only has 256 of them. A /24 for a four-host segment wastes 250 addresses. Size for expected growth plus margin, and hand out power-of-two chunks from a pool so freed blocks can be merged back.
And you mostly can’t resize later. Once things are attached to a subnet, growing it in place rarely works because the space next to it is taken. Leave gaps, and lay things out hierarchically (region, then environment, then function), so one summary route covers a whole region.
| Do | Do not |
|---|---|
| Allocate on aligned power-of-two boundaries | Start a /23 at an odd third octet |
| Pick uncommon RFC 1918 ranges and record them | Default to 10.0.0.0/16 or 192.168.1.0/24 |
| Leave gaps for growth, group by region | Pack subnets back to back with no summary path |
Use /31 on point-to-point links where supported |
Burn a /24 on a link between two routers |
| Check what your cloud reserves per subnet | Size to 2^n - 2 and assume it is exact |
The next hop can be IPv6
One last oddity, which shows how little a prefix says about forwarding. RFC 8950 [4] lets BGP announce IPv4 prefixes with an IPv6 next hop. The destination is still matched by longest prefix in the IPv4 table, but the router reaches the next device over IPv6. That’s handy in networks with IPv6-only links that still carry IPv4 traffic. What you match on (the destination prefix) and how you get there (the next hop) are separate things.
Before a production change, don’t trust the arithmetic in your head. Run it through the CIDR calculator, then look at the table you’re about to change.
Try it yourself
CIDR and IP subnet calculator
Calculate the range, mask and size of IPv4 and IPv6 networks, split them into subnets, and check what contains or overlaps what.
Open the tool