Introduction
How do you calculate CIDRs when designing VPC subnets? Do you work through them one by one with an online subnet calculator?
Or maybe you reach for the AWS Subnet Calculator, or the Excel-based subnet calculator spreadsheet referenced in AWS docs > Amazon EKS > Amazon VPC CNI?
Both are handy tools, but I ran into a wall: I needed the subnet reserved for Transit Gateway to sit at the very end of the VPC's CIDR block, and neither tool made that easy.
Here's the background. At one organization I worked with, on-premises connectivity was built using AWS Direct Connect and Transit Gateway. Whenever a new AWS account was issued, the VPC and its Transit Gateway subnet came pre-provisioned as part of the standard setup. On top of that, the Transit Gateway subnet was always carved out from the tail end of the VPC's CIDR block by design.
So when I needed to lay out subnets for a business application, I needed a tool that could compute the Transit Gateway subnet from the end of the CIDR range. None of the existing tools did that, so I built my own.
The tool is live on GitHub Pages, and the source is on GitHub.
Features
-
Variable VPC CIDR — enter any valid CIDR block (e.g.
10.0.0.0/22) - Multi-tier subnet design — add, remove, and configure tiers with custom subnet masks
- Drag-and-drop reordering — reorder tiers by dragging them into place
-
Dual AZ support — automatically allocates subnets across
ap-northeast-1aandap-northeast-1c - TGW tail allocation — tiers named "TGW" get carved out from the end of the VPC address space (toggle on/off)
- Free address space detection — surfaces every leftover range: alignment gaps, the space between forward-allocated and TGW subnets, and trailing space
- AWS reserved address aware — shows the usable host count after subtracting the 5 AWS-reserved addresses per subnet
- Japanese/English language switching — toggle the UI language with a button; auto-detects the browser's language on load
- Dark mode support — automatically adapts to the system color scheme
How the CIDR calculation works
There's no server and no database behind this tool — the entire calculation runs as bitwise arithmetic in the browser. The process breaks down into four steps.
1. Turn the CIDR string into a 32-bit integer
First, an input string like 10.0.0.0/22 gets converted into a 32-bit integer — a network address plus a total address count — so it can be manipulated with bitwise operations.
function parseVPC(cidr) {
var m = cidr.trim().match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
if (!m) return null;
var parts = m[1].split('.').map(Number);
if (parts.some(function(p) { return p < 0 || p > 255; })) return null;
var prefix = parseInt(m[2]);
if (prefix < 8 || prefix > 28) return null;
var base = ((parts[0]<<24)|(parts[1]<<16)|(parts[2]<<8)|parts[3]) >>> 0;
var total = Math.pow(2, 32 - prefix);
var network = (base & (~((1 << (32 - prefix)) - 1) >>> 0)) >>> 0;
return { base: network, prefix: prefix, total: total, end: network + total - 1 };
}
Two things are worth calling out:
-
parts[0]<<24 | parts[1]<<16 | parts[2]<<8 | parts[3]packs the four octets into a single 32-bit integer. - The
>>> 0matters. JavaScript's bitwise operators treat numbers as signed 32-bit integers, so without it, any first octet of 128 or higher would come out negative.
~((1 << (32 - prefix)) - 1) then builds the subnet mask, and base & mask rounds down to the network address. So even if you type 10.0.0.5/22, with a non-zero host part, it automatically snaps to 10.0.0.0/22.
2. Forward tiers: round up to the alignment boundary
Regular tiers like Private-App and Public get packed from the front of the VPC, AZ by AZ. The key constraint here is alignment: a /26 subnet can only start on a 64-address boundary (x.x.x.0, x.x.x.64, x.x.x.128, and so on).
var fwdCursor = vpc.base;
for (var ni = 0; ni < normalTiers.length; ni++) {
var ntier = normalTiers[ni];
var sz = Math.pow(2, 32 - ntier.mask);
for (var az = 0; az < 2; az++) {
var aligned = Math.ceil(fwdCursor / sz) * sz;
var end = aligned + sz - 1;
if (end > vpc.end) { overflow = true; break; }
allocRows.push({ kind:'subnet', tier: ntier.name, az: AZS[az], azIdx: az,
cidr: ipToStr(aligned) + '/' + ntier.mask, first: ipToStr(aligned), last: ipToStr(end),
total: sz, usable: sz-5, tail: false, startNum: aligned, endNum: end });
fwdCursor = end + 1;
}
if (overflow) break;
}
The line that does the real work is Math.ceil(fwdCursor / sz) * sz: it rounds the current cursor position up to the nearest multiple of the subnet size. That means even when one tier is packed right up against the end of the previous one, the result always snaps to a valid CIDR boundary. If the allocation would run past the end of the VPC, an overflow flag stops the calculation.
3. TGW tiers: round down to the alignment boundary, from the end
Any tier whose name contains "TGW" gets allocated in reverse, starting from the end of the VPC. This is the feature that made me build the tool in the first place.
function floorAligned(addr, size) { return Math.floor(addr / size) * size; }
var bwdCursor = vpc.end;
var tgwBuilt = [];
var tgwRev = tgwTiers.slice().reverse();
for (var gi = 0; gi < tgwRev.length; gi++) {
var gtier = tgwRev[gi];
var gsz = Math.pow(2, 32 - gtier.mask);
for (var gaz = 1; gaz >= 0; gaz--) {
var galigned = floorAligned(bwdCursor - gsz + 1, gsz);
var gend = galigned + gsz - 1;
if (galigned < vpc.base) { overflow = true; break; }
tgwBuilt.unshift({ kind:'subnet', tier: gtier.name, az: AZS[gaz], azIdx: gaz,
cidr: ipToStr(galigned) + '/' + gtier.mask, first: ipToStr(galigned), last: ipToStr(gend),
total: gsz, usable: gsz-5, tail: true, startNum: galigned, endNum: gend });
bwdCursor = galigned - 1;
}
if (overflow) break;
}
This mirrors the forward-allocation logic. bwdCursor - gsz + 1 gives a candidate starting address, and floorAligned rounds it down to a valid boundary — the mirror image of rounding up. The TGW tier list is reversed before processing, and each result is pushed onto the front of the array with unshift, so on screen the tiers still appear in their original order even though they're being filled in from the tail.
4. Detecting free address space
Once forward and tail allocations are done, all subnets get sorted by starting address, and any gap between two adjacent subnets gets recorded as free space.
var allSubnets = allocRows.concat(tgwBuilt).sort(function(a,b) { return a.startNum - b.startNum; });
var gaps = [];
var cursor = vpc.base;
for (var si = 0; si < allSubnets.length; si++) {
var s = allSubnets[si];
if (s.startNum > cursor) gaps.push({ kind:'free', startNum: cursor, endNum: s.startNum-1 });
cursor = s.endNum + 1;
}
if (cursor <= vpc.end) gaps.push({ kind:'free', startNum: cursor, endNum: vpc.end });
This is the classic "find the gaps between intervals" algorithm. A cursor walks forward from the start of the VPC; whenever there's a gap between the cursor and the next subnet, it gets recorded, and if the cursor never reaches the end of the VPC, the leftover range at the end is recorded too. The odd leftover addresses caused by alignment — the space between the last forward-packed subnet and the TGW subnets, for example — get caught by this same mechanism, with nothing falling through the cracks.
Put together, two simple algorithms — rounding up or down to an alignment boundary, and finding gaps between intervals — are enough to handle a fairly involved requirement (multiple tiers, multiple AZs, tail allocation) in a few dozen lines of serverless code.
If you found this helpful, please ⭐ the repository!
📌 You can see the entire code in My GitHub Repository.


Top comments (0)