CodeToolProCodeToolPro
GitHub
Network·9 min read

IPv4 vs IPv6: Address Formats, Limits & Migration

CodeToolPro Team·

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 (0db8db8), 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

PropertyIPv4IPv6
Address size32 bits128 bits
NotationDotted decimal 192.168.1.1Colon hex 2001:db8::1
Total addresses~4.29 × 10⁹~3.4 × 10³⁸
Typical LAN subnet/24 (254 usable hosts)/64 (1.8 × 10¹⁹ addresses)
Loopback127.0.0.1::1
Private / local range10/8, 172.16/12, 192.168/16fc00::/7 (ULA)
BroadcastYes (x.x.x.255)No — multicast only
Header size20 bytes (variable)40 bytes (fixed)
NATUbiquitousDiscouraged
In URLshttp://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:

FieldOutput
Decimal192.168.1.1
Binary11000000.10101000.00000001.00000001
HexadecimalC0.A8.01.01
Integer3232235777

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 six 0000 and 0001

Trying the other presets: ::1Loopback Address (::1); fe80::1Link-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.1192.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

  1. Storing addresses in a 32-bit column. INT UNSIGNED holds IPv4 but silently breaks the moment an IPv6 client connects. Use VARBINARY(16) or a native INET type.
  2. Forgetting brackets in URLs. http://2001:db8::1:8080/api throws Invalid URL in Node; http://[2001:db8::1]:8080/api parses fine.
  3. Validating with a naive regex. Compressed notation, mixed case, zone IDs (fe80::1%eth0), and IPv4-mapped forms defeat most hand-written patterns. Use ipaddress in Python, or a parser, not a regex.
  4. Assuming /64 behaves like /24. An IPv6 /64 contains 1.8 × 10¹⁹ addresses. Never try to enumerate one — the IPv4 Range Expander exists precisely because that is only sane for small IPv4 blocks.
  5. Treating ::ffff:a.b.c.d as IPv6-only. It is an IPv4 address in IPv6 clothing; allow-lists and geo-rules must unwrap it before matching.
  6. Comparing addresses as strings. 2001:db8::1 and 2001:0db8:0000:0000:0000:0000:0000:0001 are the same address but different strings. Normalise to the expanded form before comparing or hashing.

Related Tools

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.