What the three arguments do
cidrsubnet(prefix, newbits, netnum) takes a network and carves a smaller one out of it.
- prefix is the parent network, e.g.
"10.0.0.0/16". - newbits is how many bits longer the result is. A
/16withnewbits = 8gives a/24. It is not the resulting prefix length, which is the mistake behind most unexpected results. - netnum is the number written into those new bits, so it selects which of the
subnets you get. It must fit in
newbitsbits, i.e. be at most2newbits − 1.
Worked examples
| Call | Result | What it shows |
|---|---|---|
cidrsubnet("10.0.0.0/16", 8, 2) | 10.0.2.0/24 | Eight more bits make a /24; netnum 2 fills them with 2. |
cidrsubnet("10.0.1.5/16", 8, 2) | 10.0.2.0/24 | The host bits of the parent (.1.5) are masked away first, silently. |
cidrsubnet("10.0.0.0/16", 0, 0) | 10.0.0.0/16 | newbits of 0 returns the parent unchanged. |
cidrsubnet("10.0.0.0/16", -8, 0) | 10.0.0.0/8 | Negative newbits shorten the prefix, giving a network that is not inside the parent. |
cidrsubnet("fd00:fd12:3456:7890::/56", 16, 162) | fd00:fd12:3456:7800:a200::/72 | IPv6 works the same way; 162 is written into the 16 new bits. |
cidrsubnets("10.1.0.0/16", 4, 4, 8, 4) | 10.1.0.0/20, 10.1.16.0/20, 10.1.32.0/24, 10.1.48.0/20 | Consecutive subnets of different sizes, each aligned to its own boundary. |
cidrhost("10.12.112.0/20", 16) | 10.12.112.16 | The 17th address in the range (offsets start at 0). |
cidrhost("10.12.112.0/20", -1) | 10.12.127.255 | A negative hostnum counts back from the end, so -1 is the last address. |
cidrnetmask("172.16.0.0/12") | 255.240.0.0 | The dotted-quad mask for a prefix length. IPv4 only. |
The errors, and what they actually mean
Terraform prints these inside a Call to function "cidrsubnet" failed: diagnostic. The wording
below is the part that varies, reproduced exactly, so you can paste a failing call above and read the same
sentence with an explanation next to it.
| Call | Message | Why |
|---|---|---|
cidrsubnet("10.0.0.0/16", 8, 256) | prefix extension of 8 does not accommodate a subnet numbered 256 | 8 new bits number subnets 0–255, so 256 does not fit. The highest netnum is always 2^newbits − 1. |
cidrsubnet("10.0.0.0/16", 24, 0) | insufficient address space to extend prefix of 16 by 24 | 16 + 24 is longer than an IPv4 address. The limit is 32 bits in total, or 128 for IPv6. |
cidrsubnets("10.0.0.0/16", 1, 1, 1) | invalid argument 3: not enough remaining address space for a subnet with a prefix of 17 bits after 10.0.128.0/17 | Two /17s already fill the /16. cidrsubnets allocates in order and never reuses space. |
cidrhost("10.0.0.0/24", 256) | prefix of 24 does not accommodate a host numbered 256 | A /24 holds 256 addresses, numbered 0–255. |
cidrnetmask("fd00::/8") | IPv6 addresses cannot have a netmask: fd00::/8 | cidrnetmask is IPv4 only; IPv6 has no dotted-quad mask form. |
cidrsubnet("10.0.0.0/33", 8, 1) | invalid CIDR expression: invalid CIDR address: 10.0.0.0/33 | The prefix length is checked before anything else. Watch for /33 typed instead of /32. |
Three things that surprise people
Host bits in the parent are thrown away, silently
cidrsubnet("10.0.1.5/16", 8, 2) is not an error. The prefix is masked to
10.0.0.0/16 before anything else happens, so the answer is the same as for a clean parent. If a
variable holds an address rather than a network, you will get a plausible result computed from a network you
did not mean. The tester says so whenever it applies.
Negative newbits give you a network outside the parent
Terraform accepts a negative newbits, which makes the prefix shorter, so the result is
not contained in the network you passed in
(hashicorp/terraform#30654). It is almost
never intended, and nothing warns you at plan time.
cidrsubnets allocates in order and never backtracks
Each subnet starts at the next address aligned to its own size, so asking for a large block after several
small ones can fail with space still free earlier in the range. Sort your newbits from largest
block to smallest (smallest number first) if you run out.
cidrsubnet or cidrsubnets?
cidrsubnet gives one subnet and you choose its number, which keeps addresses stable when you add
or remove subnets later. cidrsubnets gives a list and numbers them for you, packing different
sizes without gaps — but inserting an entry in the middle shifts everything after it, and that is a rebuild
of real infrastructure. For anything already deployed, prefer explicit netnum values.
OpenTofu
OpenTofu forked Terraform and carried these functions over, so the results apply there too. The vectors below come from the Go library that both build on, not from an OpenTofu binary.
About this tool
The address arithmetic is a TypeScript port of
github.com/apparentlymart/go-cidr v1.1.1 — the
library Terraform's CIDR functions are built on — together with Go's address formatting, including which run
of zero groups in an IPv6 address collapses to ::. It is pinned by 72 golden vectors generated
by running that library, and cross-checked against the examples in Terraform's function documentation. Error
messages are reproduced from the same source rather than retyped; where this tool refuses a call that
Terraform accepts but answers incorrectly, it says so instead of claiming to quote Terraform.
One limit, stated rather than hidden: IPv4-mapped IPv6 prefixes ("::ffff:10.0.0.0/104") are
rejected here. Terraform vendors an older address parser for those, and we could not verify its behaviour
against the reference, so guessing seemed worse than declining. Everything runs in your browser; nothing is
uploaded.