IPv4 vs IPv6: Address Formats, Limits & Migration
IPv4 vs IPv6: Address Formats, Limits & Migration
Every packet your application sends is addressed with either IPv4 or IPv6, and the two families look nothing alike. IPv4 uses four decimal numbers separated by dots; IPv6 uses eight hexadecimal groups separated by colons, with a shorthand that collapses runs of zeros. If you are staring at an unfamiliar address and want to see it broken down, run the dotted-quad form through our IPv4 Address Converter or paste the colon-hex form into the IPv6 Parser / Expander.
This guide compares the two protocols across address space, notation, subnetting, and the mistakes that break code during a dual-stack migration — then shows real, reproducible outputs measured with the tools.
What Is IPv4?
IPv4 is the addressing scheme the internet was built on. An address is 32 bits,
written as four octets in decimal: 192.168.1.1. That gives 2³² ≈ 4.29 billion
unique addresses, which felt limitless in 1981 and ran out around 2011. The
workarounds — NAT, CGNAT, private ranges like 10.0.0.0/8 — are now permanent
fixtures of network design rather than temporary patches.
Because 32 bits fits in a single unsigned integer, IPv4 addresses are cheap to
store and compare. 192.168.1.1 is exactly the integer 3232235777, which is why
databases often persist IPv4 as an INT UNSIGNED column instead of a string.
What Is IPv6?
IPv6 is 128 bits — four times wider — written as eight groups of four hex digits:
2001:0db8:0000:0000:0000:0000:0000:0001. The address space is 2¹²⁸, roughly
3.4 × 10³⁸ addresses. That is not a marginal increase; it is enough to hand every
device a globally routable address and retire NAT entirely.
To keep addresses readable, IPv6 allows two abbreviations: leading zeros inside a
group may be dropped (0db8 → db8), and exactly one run of all-zero groups may
be replaced with ::. Together they turn the address above into 2001:db8::1.
Side-by-Side Comparison
| Property | IPv4 | IPv6 |
|---|---|---|
| Address size | 32 bits | 128 bits |
| Notation | Dotted decimal 192.168.1.1 | Colon hex 2001:db8::1 |
| Total addresses | ~4.29 × 10⁹ | ~3.4 × 10³⁸ |
| Typical LAN subnet | /24 (254 usable hosts) | /64 (1.8 × 10¹⁹ addresses) |
| Loopback | 127.0.0.1 | ::1 |
| Private / local range | 10/8, 172.16/12, 192.168/16 | fc00::/7 (ULA) |
| Broadcast | Yes (x.x.x.255) | No — multicast only |
| Header size | 20 bytes (variable) | 40 bytes (fixed) |
| NAT | Ubiquitous | Discouraged |
| In URLs | http://192.168.1.1:8080/ | http://[2001:db8::1]:8080/ |
The single most common source of bugs is the last row: IPv6 literals must be wrapped in square brackets inside a URL, otherwise the colons are ambiguous with the port separator.
Hands-on: Tested with the Tool
All outputs below were produced by pasting the inputs into the CodeToolPro tools and reading the rendered fields. They are reproducible — the conversions are pure functions with no randomness.
Test 1 — IPv4 in four representations. In the
IPv4 Address Converter I left Input Format on
Decimal (192.168.1.1) and typed 192.168.1.1. All four output cards filled in
immediately:
| Field | Output |
|---|---|
| Decimal | 192.168.1.1 |
| Binary | 11000000.10101000.00000001.00000001 |
| Hexadecimal | C0.A8.01.01 |
| Integer | 3232235777 |
Test 2 — round trip from the integer. Switching Input Format to Integer
and entering 3232235777 reproduced 192.168.1.1 exactly, confirming the
conversion is lossless in both directions. Entering 8.8.8.8 in decimal mode
returned integer 134744072 and hex 08.08.08.08 (note the zero padding per
octet).
Test 3 — invalid input fails silently. Typing 256.1.1.1 left every output
card showing -. There is no red error banner; the blank result is the
validation signal. The same happens if you paste an IPv6 address into the IPv4
tool.
Test 4 — IPv6 expansion and type detection. Pasting 2001:db8::1 into the
IPv6 Parser / Expander returned:
- Address Type:
Documentation Address (2001:db8::/32) - Expanded Form:
2001:0db8:0000:0000:0000:0000:0000:0001 - Compressed Form:
2001:db8::1 - Groups: eight cards,
2001,0db8, then six0000and0001
Trying the other presets: ::1 → Loopback Address (::1); fe80::1 →
Link-Local Unicast (fe80::/10); 2002:c0a8:0101:: →
6to4 Transition Address (2002::/16), compressed back to 2002:c0a8:101:: (the
leading zero of 0101 is stripped, which is correct RFC 5952 formatting).
Test 5 — an honest edge case. The parser's own preset button
::ffff:192.168.1.1 does not produce an IPv4-mapped result. The dotted-quad
tail is read as hexadecimal, so the last group becomes 0192 and the type reads
Other. Entering the equivalent pure-hex form ::ffff:c0a8:0101 works correctly:
type IPv4-mapped IPv6 Address, and an extra Embedded IPv4 card showing
192.168.1.1. If you need to inspect a mapped address with this tool, convert the
dotted quad to hex first — the IPv4 Address Converter
gives you C0.A8.01.01, which becomes c0a8:0101.
Test 6 — subnet math. In the CIDR Calculator,
192.168.1.0/24 returned network 192.168.1.0, broadcast 192.168.1.255, usable
range 192.168.1.1–192.168.1.254, 254 total hosts, mask 255.255.255.0,
wildcard 0.0.0.255, class C. Entering a mid-block host, 192.168.1.130/26,
correctly snapped the network down to 192.168.1.128 with broadcast
192.168.1.191 and 62 usable hosts. A point-to-point 203.0.113.5/31 reported
2 hosts and N/A (point-to-point) for first/last usable — the correct RFC 3021
behaviour.
Code Examples
Both snippets below were executed locally (Node 22 / Python 3.13) and the printed values are the real output.
function ipv4ToInt(ip) {
const parts = ip.split(".").map(Number);
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
throw new Error(`Not an IPv4 address: ${ip}`);
}
return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
}
function expandIPv6(addr) {
const [head, tail = ""] = addr.split("::");
const left = head ? head.split(":") : [];
const right = tail ? tail.split(":") : [];
const fill = Array(8 - left.length - right.length).fill("0");
return [...left, ...(addr.includes("::") ? fill : []), ...right]
.map((g) => parseInt(g || "0", 16).toString(16).padStart(4, "0"))
.join(":");
}
ipv4ToInt("192.168.1.1"); // 3232235777
expandIPv6("2001:db8::1"); // "2001:0db8:0000:0000:0000:0000:0000:0001"
new URL("http://[2001:db8::1]:8080/api").hostname; // "[2001:db8::1]"
Python ships a full implementation in the standard library, so prefer it over hand-rolled parsing:
import ipaddress
v4 = ipaddress.ip_address("192.168.1.1")
print(v4.version, int(v4)) # 4 3232235777
v6 = ipaddress.ip_address("2001:db8::1")
print(v6.version, v6.exploded) # 6 2001:0db8:0000:0000:0000:0000:0000:0001
net = ipaddress.ip_network("192.168.1.0/24")
print(net.netmask, net.broadcast_address, net.num_addresses - 2)
# 255.255.255.0 192.168.1.255 254
print(ipaddress.ip_network("2001:db8::/64").num_addresses)
# 18446744073709551616
Note how v6.exploded matches the parser's Expanded Form character for
character, and the /24 figures match the CIDR Calculator exactly.
Common Mistakes
- Storing addresses in a 32-bit column.
INT UNSIGNEDholds IPv4 but silently breaks the moment an IPv6 client connects. UseVARBINARY(16)or a nativeINETtype. - Forgetting brackets in URLs.
http://2001:db8::1:8080/apithrowsInvalid URLin Node;http://[2001:db8::1]:8080/apiparses fine. - Validating with a naive regex. Compressed notation, mixed case, zone IDs
(
fe80::1%eth0), and IPv4-mapped forms defeat most hand-written patterns. Useipaddressin Python, or a parser, not a regex. - Assuming
/64behaves like/24. An IPv6/64contains 1.8 × 10¹⁹ addresses. Never try to enumerate one — the IPv4 Range Expander exists precisely because that is only sane for small IPv4 blocks. - Treating
::ffff:a.b.c.das IPv6-only. It is an IPv4 address in IPv6 clothing; allow-lists and geo-rules must unwrap it before matching. - Comparing addresses as strings.
2001:db8::1and2001:0db8:0000:0000:0000:0000:0000:0001are the same address but different strings. Normalise to the expanded form before comparing or hashing.
Related Tools
- Convert IPv4 between decimal, binary, hex, and integer with the IPv4 Address Converter.
- Expand, compress, and classify IPv6 addresses with the IPv6 Parser / Expander.
- Compute masks, ranges, and host counts with the CIDR Calculator.
- Generate RFC 4193 private IPv6 prefixes with the IPv6 ULA Generator.
- List every address in a small block with the IPv4 Range Expander.
- Check how a host and port parse out of a URL with the URL Parser, covered further in HTTP vs HTTPS.
When to Use This Tool Instead of Code
Writing ipaddress.ip_address(x) takes one line, so the tools are not there to
replace your runtime — they are there for the moments when you are reading
addresses rather than processing them. Debugging a firewall rule, sanity-checking a
subnet a colleague proposed, or decoding a log line where the address arrived as an
integer are all faster in a browser tab than in a REPL, and the
IPv4 Address Converter shows all four representations
at once instead of one per call.
The CIDR Calculator is the strongest case. Prefix
arithmetic is easy to get subtly wrong by hand — /26 boundaries in particular —
and seeing the network, broadcast, usable range, and wildcard mask side by side
catches off-by-one errors before they reach a security group. For anything running
in production, keep the logic in code; for the five-minute question in front of
you, the tool is the shorter path.