← Math in Practice
Concept Address arithmetic and allocation

Network subnet planning

Size the blocks, align their boundaries, then check what your address plan cannot tell you.

A team is splitting a private cloud network across three availability zones. The application tier needs room for today's endpoints and planned growth; the database and public ingress tiers have different sizes. An address plan that adds up on a spreadsheet can still overlap, start at an invalid boundary, or leave too little room after the cloud reserves addresses.

The judgment to keep

Turn a host requirement into a power-of-two block, choose a prefix with explicit growth and reservation policy, and verify each proposed range is aligned, contained, and non-overlapping. That proves an allocation fits on paper; routing and policy decide whether packets can reach it.

TypeScriptGo IPv4 CIDR · address capacity · growth margin · alignment · overlap
01 / Read the capacity request

Nine subnet requests must fit inside one private range.

Consider an illustrative VPC range 10.42.0.0/20. The application tier will occupy one subnet in each of three zones. Each zone has 420 current endpoints and a planning allowance of 25% growth: 420 × 1.25 = 525 endpoints. The database tier has 180 current endpoints per zone plus 25% growth, or 225. Public ingress needs 36 addresses per zone plus 25%, or 45.

These figures are planning assumptions, not cloud telemetry. We will use the AWS IPv4 subnet reservation rule as an explicit example: AWS reserves five IPv4 addresses in every subnet. Other platforms, on-premises networks, and special subnet types may reserve different ranges. We still need to account for infrastructure interfaces and any addresses the team intentionally leaves unused.

Case file / Three-zone network planCan all requested tiers fit, with growth, in 10.42.0.0/20?
Application
3 × 525 endpoint capacity after growth allowance.
Database
3 × 225 endpoint capacity after growth allowance.
Public ingress
3 × 45 endpoint capacity after growth allowance.
Allocation policy
Reserve five IPv4 addresses per subnet for this AWS-style example.
02 / Size each requirement

A prefix describes a block; the usable count depends on policy.

IPv4 has 32 bits. A /p prefix fixes p network bits and leaves 32 − p host bits, so a block contains 232−p addresses. If the platform reserves five addresses, its assignable count in this example is the block size minus five. The smallest prefix is the longest prefix whose assignable capacity still meets the requirement.

For application capacity, a /23 has 29 = 512 addresses and AWS-style usable capacity 512 − 5 = 507, short of 525. A /22 has 210 = 1,024, with 1,019 assignable. For the database requirement, /25 gives 123, while /24 gives 251. For ingress, /27 gives 27, while /26 gives 59. The chosen blocks therefore allocate 3 × (1,024 + 256 + 64) = 4,032 addresses from the VPC's 4,096-address range.

The 64-address remainder is not an arbitrary 64-address subnet unless it is on a /26 boundary; here the remaining range happens to be 10.42.15.192/26. The plans also meet only the stated endpoint totals. If each endpoint needs multiple interfaces, a load balancer, NAT addresses, or reserved room for rollout overlap, include those before selecting the prefix.

One subnet in each zone · five-address AWS reservation assumption
Tier per zoneDemand after growthChosen prefixTotal / assignable¹Why the next smaller block fails
Application525/221,024 / 1,019/23 gives 507
Database225/24256 / 251/25 gives 123
Ingress45/2664 / 59/27 gives 27

¹ Assignable counts subtract five addresses according to the AWS IPv4 subnet rule used in this example. Confirm the reservation behavior and service quotas for the actual platform.

03 / Place the blocks

Every prefix has a boundary it must start on.

A /22 contains 1,024 consecutive addresses, so its network address must be a multiple of 1,024 in the 32-bit address space. In this VPC the /22 boundaries appear at 10.42.0.0, 10.42.4.0, 10.42.8.0, and 10.42.12.0. The three application ranges can occupy the first three. The database /24 ranges then begin at 10.42.12.0, 10.42.13.0, and 10.42.14.0. Three /26 ingress ranges fit in the final /24 at offsets 0, 64, and 128.

Read the suffix as an inclusive range: 10.42.8.0/22 spans 10.42.8.0–10.42.11.255. A candidate like 10.42.1.0/22 is not a distinct /22 starting at .1. Applying a /22 mask yields the containing network 10.42.0.0/22; the entered address has host bits set and is not a canonical network address.

One valid contiguous allocation / 4,096 total addresses
10.42.0.0/22Application · 1,024
10.42.4.0/22Application · 1,024
10.42.8.0/22Application · 1,024
10.42.12.0/24Database
10.42.13.0/24Database
10.42.14.0/24Database
.15.0/26Ingress
.15.64/26Ingress
.15.128/26Ingress
.15.192/26Unallocated
04 / Audit the plan

A valid plan must pass more than a capacity sum.

For each candidate, record its CIDR, inclusive start and end, total addresses, provider-assignable addresses, owner, zone, and purpose. Check that the CIDR is canonical, the whole child block is contained in the VPC, and no two allocations overlap. Then reserve unallocated ranges deliberately. A plan with a small leftover may be technically valid and still be hard to expand without renumbering.

Also compare the proposed ranges with other networks the system may connect: corporate LANs, partner VPNs, peered VPCs, container overlays, and disaster-recovery regions. Two individually valid private ranges can collide when routed together. Address planning happens at the system boundary, not only inside one VPC spreadsheet.

Allocation review / required checks

Capacity: endpoint demand + stated growth + infrastructure reservations.

Prefix: next power-of-two block that meets usable capacity.

Placement: canonical network address aligned to the block size.

Safety: contained, no overlap, owner and expansion space recorded.

Passes address arithmetic ≠ approved network design.

05 / Practice in code

Let validation catch a host address masquerading as a subnet.

The examples parse IPv4 prefixes, reject host bits, check that all blocks are inside the VPC, and detect overlapping ranges. Both print the total size and a five-address policy-adjusted count. That last subtraction is intentionally labeled as a policy: change it if the target platform reserves a different set.

Compare the same allocation checks in TypeScript and Go.

Both implementations validate canonical prefixes, containment, overlap, and block size.

TypeScriptIPv4 subnet arithmetic · aligned non-overlapping blocks
subnets.ts
type Prefix = { cidr: string; start: number; end: number; prefix: number; total: number };

function ipv4ToNumber(address: string): number {
	if (!/^(?:0|[1-9]\d{0,2})(?:\.(?:0|[1-9]\d{0,2})){3}$/.test(address)) {
		throw new Error(`Invalid IPv4 address: ${address}`);
	}
	const octets = address.split('.').map(Number);
	if (
		octets.length !== 4 ||
		octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
	) {
		throw new Error(`Invalid IPv4 address: ${address}`);
	}
	return octets.reduce((value, part) => value * 256 + part, 0);
}

function numberToIPv4(value: number): string {
	if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff)
		throw new Error('Address out of range.');
	return [24, 16, 8, 0].map((shift) => Math.floor(value / 2 ** shift) % 256).join('.');
}

function parsePrefix(cidr: string): Prefix {
	const [address, prefixText, extra] = cidr.split('/');
	const prefix = Number(prefixText);
	if (extra !== undefined || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) {
		throw new Error(`Invalid IPv4 prefix: ${cidr}`);
	}
	const ip = ipv4ToNumber(address);
	const total = 2 ** (32 - prefix);
	const start = Math.floor(ip / total) * total;
	if (ip !== start)
		throw new Error(`${cidr} has host bits set; use ${numberToIPv4(start)}/${prefix}.`);
	return { cidr, start, end: start + total - 1, prefix, total };
}

function overlaps(a: Prefix, b: Prefix): boolean {
	return a.start <= b.end && b.start <= a.end;
}

const vpc = parsePrefix('10.42.0.0/20');
const allocations = [
	'10.42.0.0/22',
	'10.42.4.0/22',
	'10.42.8.0/22',
	'10.42.12.0/24',
	'10.42.13.0/24',
	'10.42.14.0/24',
	'10.42.15.0/26',
	'10.42.15.64/26',
	'10.42.15.128/26'
].map(parsePrefix);

for (let index = 0; index < allocations.length; index += 1) {
	const block = allocations[index];
	if (block.start < vpc.start || block.end > vpc.end)
		throw new Error(`${block.cidr} is outside ${vpc.cidr}.`);
	if (allocations.slice(0, index).some((earlier) => overlaps(earlier, block)))
		throw new Error(`${block.cidr} overlaps an earlier allocation.`);
}

for (const block of allocations) {
	console.log(
		`${block.cidr}: ${block.total} total, ${block.total - 5} usable under the stated cloud reservation policy`
	);
}
GoIPv4 subnet arithmetic · aligned non-overlapping blocks
subnets.go
package main

import (
	"fmt"
	"net/netip"
)

func parseNetwork(cidr string) (netip.Prefix, error) {
	prefix, err := netip.ParsePrefix(cidr)
	if err != nil {
		return netip.Prefix{}, err
	}
	if !prefix.Addr().Is4() {
		return netip.Prefix{}, fmt.Errorf("%s is not IPv4", cidr)
	}
	if prefix != prefix.Masked() {
		return netip.Prefix{}, fmt.Errorf("%s has host bits set; use %s", cidr, prefix.Masked())
	}
	return prefix, nil
}

func totalAddresses(prefix netip.Prefix) uint64 {
	return uint64(1) << uint(32-prefix.Bits())
}

func contains(outer, inner netip.Prefix) bool {
	return outer.Contains(inner.Addr()) && outer.Contains(lastAddress(inner))
}

func lastAddress(prefix netip.Prefix) netip.Addr {
	base := prefix.Addr().As4()
	value := uint64(base[0])<<24 | uint64(base[1])<<16 | uint64(base[2])<<8 | uint64(base[3])
	value += totalAddresses(prefix) - 1
	return netip.AddrFrom4([4]byte{byte(value >> 24), byte(value >> 16), byte(value >> 8), byte(value)})
}

func main() {
	vpc, err := parseNetwork("10.42.0.0/20")
	if err != nil {
		panic(err)
	}
	blocks := []string{
		"10.42.0.0/22", "10.42.4.0/22", "10.42.8.0/22",
		"10.42.12.0/24", "10.42.13.0/24", "10.42.14.0/24",
		"10.42.15.0/26", "10.42.15.64/26", "10.42.15.128/26",
	}
	used := make([]netip.Prefix, 0, len(blocks))
	for _, cidr := range blocks {
		block, err := parseNetwork(cidr)
		if err != nil {
			panic(err)
		}
		if !contains(vpc, block) {
			panic(fmt.Sprintf("%s is outside %s", block, vpc))
		}
		for _, prior := range used {
			if prior.Overlaps(block) {
				panic(fmt.Sprintf("%s overlaps %s", block, prior))
			}
		}
		used = append(used, block)
		fmt.Printf("%s: %d total, %d usable under the stated cloud reservation policy\n", block, totalAddresses(block), totalAddresses(block)-5)
	}
}
06 / Keep routing separate

An address can fit and still be unreachable.

CIDR arithmetic describes address ranges and prefix matching. Reachability depends on the forwarding path and policy: route tables, next hops, peering or VPN state, network ACLs, security groups or host firewalls, service listeners, and return routes. A subnet can be perfectly sized and allocated but still have no route to a destination or an allowed port.

When a deployment fails, diagnose the layer with evidence. Confirm the destination address and source subnet; inspect the most-specific route and target; check whether the next hop is healthy; inspect filtering in both directions; then verify the listener and application response. Do not resize a subnet to repair a missing route. Conversely, a route that exists does not prove that address assignment or return traffic is correct.

07 / Review a changed demand

Growth, reservations, and connected ranges can change the answer.

Suppose application demand rises from 420 to 700 endpoints per zone before the same 25% growth margin. The plan needs 700 × 1.25 = 875 assignable addresses. A /22 still provides 1,019 under this reservation policy, but the original plan's address count alone cannot tell whether the service supports that many interfaces or whether other tiers can still fit.

For practice, recalculate the database and ingress prefixes if those tiers double their current endpoint counts before growth. Then ask what happens if a newly peered corporate network already uses 10.42.12.0/24. The local plan still fits numerically, but the overlap may make the connected design ambiguous or require renumbering.

Carry the decision into a real design

  1. List current endpoint counts per zone and identify infrastructure interfaces that consume addresses.
  2. Apply growth as a stated factor, then select the smallest block that passes the actual provider's assignable-address rule.
  3. Write each proposed CIDR in canonical form and record its full start-to-end range.
  4. Compare allocations with all networks that need to communicate, not just siblings in this VPC.
  5. After allocation, test the route and filtering path independently.

Transfer: If the target is an on-premises /31 point-to-point link, do not reuse the usual “subtract network and broadcast” shortcut without checking the link convention and equipment support.