The 30-Year Migration Nobody Finished
IPv6: Technically Superior, Practically Optional
Reading time: ~16 minutes
In the last post I covered HTTP — the application layer protocol that went from text-based (HTTP/1.1) to binary multiplexed (HTTP/2) to "let's ditch TCP entirely" (HTTP/3). HTTP is the sibling protocol to IP in the sense that matters: they're both load-bearing infrastructure that the entire internet runs on, they both have a version everyone uses and a version everyone should use, and they both have a migration story that makes engineers drink.
HTTP's migration took about five years. HTTP/2 shipped in 2015, and by 2020 a majority of web traffic was running over it. IPv6's migration has been running since 1998. It is now 2026. The migration is "almost done." It has been "almost done" for longer than most working developers have been alive.
This is the story of a protocol that is objectively better than the thing it replaces, designed by serious people to solve a real problem, backed by every major standards body and vendor on the planet -- and it still hasn't won. Not because it's flawed. Because the thing it was supposed to replace got a hack bolted onto it that was good enough.
That hack is NAT. And "good enough" is the most dangerous phrase in infrastructure.
And before you ask — yes, there was an IPv5. No, it wasn't some family embarrassment nobody speaks of. The version number was assigned to the Internet Stream Protocol (ST) family — an experimental real-time streaming protocol that first showed up in IEN 119 in 1979, got a more formal revision in RFC 1190 (1990), and another in RFC 1819 (1995). None of them left the lab in any serious way. The version number was taken, so the next real version of IP got 6. That's it. Just a version numbering collision, not a buried scandal. No Kennedy, no car, no bridge.
The Address That Ran Out
The IPv4 address is 32 bits. That gives you 2^32 addresses: 4,294,967,296. Four billion and change. When Vint Cerf and Bob Kahn were designing the Internet Protocol in the late 1970s, four billion addresses for a research network connecting a few hundred machines seemed absurdly generous.
It wasn't.
By the mid-1990s, people could see the wall. The internet was growing exponentially, and the address space was being consumed faster than anyone had projected. The math wasn't complicated -- it was just uncomfortable. In February 2011, IANA allocated the last blocks of IPv4 address space to the five Regional Internet Registries. APNIC (Asia-Pacific) ran out months later. RIPE (Europe) in 2012. LACNIC (Latin America) in 2014. ARIN (North America) in 2015.

"Ran out" is slightly dramatic. What actually happened is that the free pool dried up. Addresses still exist -- they're held by organisations, traded on secondary markets (yes, there's a market for IPv4 addresses, with prices that have hovered in the tens of dollars per address for years and bounce around with supply and cloud-provider hoarding), and recycled from legacy allocations. MIT gave back a /8 block. The US Department of Defense has been slowly relinquishing space it hasn't used since the 1980s. But the days of walking up to ARIN and asking for a /16 because you're starting a company are over.
The IETF saw this coming in the early 1990s and started designing a replacement. That replacement became IPv6.
What an IPv6 Address Looks Like
An IPv4 address is 32 bits, written as four decimal octets: 192.168.1.1. You can read it aloud. You can memorise your home router's address. You can scribble it on a napkin.
An IPv6 address is 128 bits. Written as eight groups of four hexadecimal digits, separated by colons:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
That's 340 undecillion addresses. 340,282,366,920,938,463,463,374,607,431,768,211,456, if you want to feel the number. Enough to assign an address to every atom on the surface of the Earth and still have addresses left over. The IPv6 designers did not want to have this meeting again.
There are abbreviation rules because nobody is typing that full thing. Leading zeros in a group can be dropped: 0db8 becomes db8. One consecutive run of all-zero groups can be collapsed to ::. So the loopback address, which is 0000:0000:0000:0000:0000:0000:0000:0001, becomes:
::1
That's localhost. The IPv6 version of 127.0.0.1. Clean. Elegant. Completely unreadable in a URL because you have to wrap it in square brackets: http://[::1]:8080/. Every web developer who has tried to paste an IPv6 address into a config file, a database connection string, or a YAML file has a war story. Colons are already meaningful in URIs. The bracket syntax is a band-aid.
Try reading an IPv6 address aloud on a phone call. "Two-zero-zero-one colon zero-delta-bravo-eight colon..." I have done this. I don't recommend it.
The Special Addresses
A few worth knowing:
::1 -- loopback. Same as 127.0.0.1.
fe80::/10 -- link-local addresses. Every IPv6 interface auto-generates one of these the moment you bring it up, whether or not you ever configured IPv6. Run ip addr on any modern Linux box and there's a fe80::... entry sitting there waiting. They only work on the local network segment — you can't route them past the switch — and they look like fe80::1a2b:3c4d:5e6f:7890%eth0. That %eth0 suffix is the zone ID, and it's there because link-local addresses are only meaningful in the context of a specific interface. The same fe80::1 on eth0 and wlan0 refers to two different machines. Pass a link-local address into a socket API without the zone ID and you'll get mysterious errors.
2000::/3 -- global unicast. The routable internet. If you have an IPv6 address that starts with 2 or 3, it's a public, globally routable address. No NAT. No translation. That device is directly addressable from anywhere on the internet.
fc00::/7 -- unique local. IPv6's answer to 10.0.0.0/8, which almost nobody bothers with because the whole point of IPv6 was to not need private ranges.
The Design Philosophy: No More Hacks
IPv6 wasn't designed by people who wanted a bigger number. It was designed by people who wanted to fix the architectural mistakes of IPv4 — the same instinct that drove HTTP/2's redesign of HTTP/1.1's head-of-line blocking. Sibling protocols, same impulse: the old version works but the architecture is wrong, so let's fix it properly. The address space is the headline, but the real changes are deeper.
No NAT. This was the point. Every device gets a globally unique, publicly routable address. Your phone. Your fridge. Your light bulb. Your thermostat. All directly addressable. End-to-end connectivity -- the original design principle of the internet that NAT broke. I'll come back to why this matters and why it didn't matter enough.
No broadcast. IPv4 has broadcast -- send a packet to 255.255.255.255 and every device on the subnet processes it. This is a terrible idea at scale. IPv6 replaces broadcast entirely with multicast (one-to-many with explicit group membership) and anycast (one-to-nearest). Your network doesn't wake up every device every time someone sends a broadcast storm.
Simplified header. The IPv4 header is variable-length with a checksum field and options that almost nobody uses but every router must parse. The IPv6 header is fixed at 40 bytes. No checksum -- TCP and UDP have their own checksums, and the link layer (Ethernet) has its own CRC, so the IP-layer checksum was redundant work at every hop. No options in the base header -- extension headers are daisy-chained after the fixed header, and routers only inspect the ones they need to.
Auto-configuration. IPv6 has SLAAC -- Stateless Address Autoconfiguration. A device can generate its own globally unique address by combining the network prefix (announced by the router) with an identifier derived from its MAC address (or a random value for privacy). No DHCP server required. Plug in, listen for a router advertisement, generate your address, done. DHCP still exists in IPv6 (DHCPv6), but it's optional, not mandatory.

On paper, this is all better. Genuinely, objectively better. The header is faster to process. The address space will never run out. Auto-configuration reduces operational overhead. End-to-end connectivity enables peer-to-peer applications without NAT traversal gymnastics.
The people who designed IPv6 were right about everything. And it didn't matter.
The Hack That Ate the Internet
In 1994, RFC 1631 introduced Network Address Translation. It was explicitly described as a "short-term solution" while IPv6 deployment ramped up. A stopgap. A bridge. Remember that workaround you put in once because the full refactor was planned for next year? Well this is that at a global scale.
NAT works like this: your home router has one public IPv4 address. Behind it, you have 47 devices, each with a private address from 192.168.x.x. When your laptop sends a packet to the internet, the router rewrites the source address from 192.168.1.42 to its own public IP, notes the mapping in a table, and forwards the packet. When the reply comes back, the router looks up the mapping and forwards the packet to your laptop. From the internet's perspective, there's one device. Behind the router, there are 47.

This solved the address exhaustion problem overnight. One public IP per household instead of one per device. Enterprises could use 10.0.0.0/8 internally -- that's 16,777,216 addresses from a single private range -- and expose as few public IPs as they needed. The pressure on the IPv4 address space dropped dramatically.
NAT was supposed to buy five years. It bought thirty. And counting.
The reason is uncomfortable for protocol purists: NAT doesn't just conserve addresses. It accidentally provides a security boundary. Your laptop at 192.168.1.42 is not directly addressable from the internet. Nothing can initiate a connection to it without the router having a mapping. For the average home user, NAT is a firewall they never had to configure. This was never the intent -- NAT is not a security mechanism and relying on it as one is architecturally wrong -- but it's how people experience it.
IPv6's promise of "every device gets a public address" sounded like a feature to network engineers and like a threat to everyone else. "You mean my smart toaster is directly addressable from the internet?" Yes, technically, with a firewall on the router doing the actual access control instead of NAT doing it accidentally. But that's a harder sell than "your stuff is hidden behind one IP."
The Dual-Stack Reality
Most networks today run both protocols simultaneously. Your machine probably has both an IPv4 and an IPv6 address right now. You can check:
# Check your addresses
ip -4 addr show # IPv4 addresses
ip -6 addr show # IPv6 addresses
# Test connectivity
curl -4 ifconfig.me # Your public IPv4 address
curl -6 ifconfig.me # Your public IPv6 address (if you have one)
import socket
# Get address info for both protocols
infos = socket.getaddrinfo("google.com", 443)
for family, _, _, _, addr in infos:
proto = "IPv4" if family == socket.AF_INET else "IPv6"
print(f"{proto}: {addr[0]}")
addrs, _ := net.LookupHost("google.com")
for _, addr := range addrs {
ip := net.ParseIP(addr)
if ip.To4() != nil {
fmt.Println("IPv4:", addr)
} else {
fmt.Println("IPv6:", addr)
}
}
The beautiful thing -- or the damning thing, depending on your perspective -- is that most developers never think about this. The function that handles it is getaddrinfo(), and it returns both IPv4 and IPv6 addresses for a hostname. Your HTTP library picks one. Your code doesn't know or care which protocol carried the bytes.
If you've been writing web applications, APIs, microservices, or anything that connects over the network using hostnames instead of hardcoded IPs, you've probably been using IPv6 without knowing it. The abstraction works.
This is also why nobody is in pain. The dual-stack approach means you don't have to choose. Your code works on both. Your DNS has both A records (IPv4) and AAAA records (IPv6) -- four As because, as the widely accepted explanation goes, IPv6 addresses are four times longer than IPv4 addresses 🤣.
Happy Eyeballs
There's an algorithm called Happy Eyeballs. I'm not making that up. RFC 8305, published in 2017. And it's absolutely fantastic. Find it. Read it. It's my canonical example when teaching async coding.
Here's the problem it solves: your machine has both IPv4 and IPv6 connectivity, and a DNS lookup returns both A and AAAA records for a destination. Which do you connect to? If you try IPv6 first and the path is broken (which happens -- IPv6 connectivity is less reliable than IPv4 in many networks), you wait for a timeout before falling back to IPv4. That timeout is visible to users. Pages load slowly. APIs feel sluggish.
Happy Eyeballs races the connections. The algorithm starts an IPv6 connection attempt, and if it hasn't succeeded within a short delay — 250 ms is the RFC 8305 recommended default, but implementations tune it (Chrome historically used 300 ms) — it starts an IPv4 attempt in parallel. Whichever completes the TCP handshake first wins. The loser gets torn down.
Every modern browser implements Happy Eyeballs. Chrome, Firefox, Safari, Edge. curl has had a --happy-eyeballs-timeout-ms option since 7.59.0 (2018). Go's standard net package does it, Rust does it via tokio and friends, and recent Python's getaddrinfo-based stack does it too.
This is what makes dual-stack invisible to users. If your IPv6 path is broken, your browser silently falls back to IPv4 in a quarter of a second. You never see an error. You never notice the difference.
It also means IPv6 only wins the race when IPv6 is faster. Which, in a world where IPv4 infrastructure has had 40 years of optimisation, is not always the case.
Why Corporate Networks Don't Bother
I've worked in multiple enterprises where the internal network was pure IPv4. The argument against IPv6 migration was always the same, and it was always reasonable:
10.0.0.0/8 gives you 16 million internal addresses. For a company with 50,000 employees and 200,000 devices, that's 80 times more address space than they need internally. There is no address pressure.
The monitoring tools, firewall rules, ACLs, IPAM systems, and network team muscle memory are all IPv4. Migration means rewriting all of it. Not because IPv4 is broken, but because a standards body says you should.
The benefit is invisible. The cost is real. The migration gets filed under "someday" and someday never comes.
I don't blame them. If I had a network that worked, a team that understood it, and a CFO asking me to justify the budget, "the IETF designed a better protocol in 1998" would not be a compelling pitch.
Where IPv6 Actually Matters
There are three places where IPv6 isn't optional anymore.
Mobile networks. The major carriers have been the most aggressive IPv6 adopters, and for a simple reason: they ran out of IPv4 addresses years ago. T-Mobile US runs a predominantly IPv6 network. When your phone connects to a cell tower, it gets an IPv6 address. If it needs to reach an IPv4-only server, the carrier runs NAT64/DNS64 translation at the edge. Your phone doesn't know. The carrier doesn't care. They saved millions on IPv4 address costs.
IoT at scale. If you're deploying tens of thousands of sensors, meters, or controllers, giving each one a public IPv4 address is economically and technically absurd. IPv6 with SLAAC -- plug in, auto-configure, globally addressable -- is exactly what it was designed for. Smart grid deployments, industrial sensor networks, and city-scale IoT projects are IPv6-native because the alternative doesn't scale.
Cloud infrastructure. This one hit developers in the wallet. In February 2024, AWS started charging $0.005 per hour for every public IPv4 address. That's $3.60 per month, $43.80 per year, per address. If you're running 100 instances with public IPs, that's $4,380 a year in pure address tax. Azure and GCP have similar pricing pressures. IPv6 addresses remain free. When I saw teams scrambling to move services behind load balancers and switch to IPv6 endpoints after that announcement, I thought: this is the thing that will actually drive adoption. Not technical superiority. Invoice line items.

Google's IPv6 statistics page — the closest thing there is to a canonical adoption meter — has the curve climbing steadily and crossing the 45% line around early 2026. That number is misleading in both directions. Among mobile users and content delivery networks, adoption is much higher. Among enterprise internal networks and legacy infrastructure, it's much lower. The 45% is an average that describes nobody's actual experience.
My Take
IPv6 is a better protocol. The address space will last effectively forever. We've seen this exact pattern before — in post 10 (malloc) I covered the jump from 32-bit to 64-bit virtual memory. 2^32 bytes (4GB) seemed absurd in the 1980s. We hit the wall by 2003. The fix was the same: jump to a number space so large that human physics can't exhaust it. 2^64 bytes of virtual address space is more memory than we can physically manufacture. 2^128 IP addresses is more addresses than there are atoms on the surface of the Earth. Both jumps solve the problem permanently, not by being clever, but by making the number big enough that "running out" stops being a meaningful concept. The header design is cleaner. SLAAC is elegant. End-to-end connectivity without NAT is architecturally correct.
None of that matters as much as the fact that NAT works.
NAT was a hack. It violated the end-to-end principle. It broke peer-to-peer applications. It created an entire industry of NAT traversal techniques (STUN, TURN, ICE) that wouldn't need to exist if every device had a public address. It made certain classes of application -- peer-to-peer file sharing, direct device-to-device communication, hosting a server from your home network -- unnecessarily difficult.
But it solved the problem it was asked to solve. And in doing so, it removed the pain that would have driven IPv6 adoption.
This is a pattern I see everywhere in infrastructure. The technically correct solution loses to the good-enough hack, not because people are stupid or lazy, but because migration cost is real and migration benefit is invisible when the hack is working. Nobody wakes up in the morning angry about NAT. Nobody's production system is down because of IPv4. The urgency isn't there.
Compare this to the sibling protocol from last post. HTTP/2 displaced HTTP/1.1 in about five years. Why? Because the benefit was visible — pages loaded faster, multiplexing eliminated the six-connection hack, and you could measure the improvement with a stopwatch. IPv6's benefits are invisible to end users. Your browser doesn't load faster on IPv6. Your API doesn't return lower-latency responses. The improvement is architectural, and architectural improvements don't drive adoption. Visible pain does.
IPv6 will win eventually. The economics are shifting -- AWS charging for IPv4 addresses is more effective than 25 years of IETF advocacy. Mobile networks have already moved. IoT can't work without it. The dual-stack period will stretch for another decade, maybe two, and then one day a junior engineer will ask "what was IPv4?" and a senior engineer will sigh and say "it was 32 bits and we made it work for 50 years with a hack called NAT."
But that day isn't today. Today, most developers can write getaddrinfo(), let the OS handle it, and never think about IP versions at all.
Which, if you think about it, might be the real success story. Not IPv6 replacing IPv4. But the abstraction layer getting good enough that the version of IP underneath stopped mattering.
Further Reading
- Post 07: Your DNS is Lying to You -- DNS resolution, including AAAA records (the IPv6 record type). If you're curious why the record is called "AAAA," it's because the address is four times longer than an A record. Engineers.
- Post 08: What "Connected" Means in TCP -- TCP sits on top of IP. The three-way handshake works identically on IPv4 and IPv6, but Happy Eyeballs races the handshake on both protocols simultaneously.
- Post 16: The Invisible Negotiation Between Your Laptop and the Air -- WiFi operates at Layer 2, below IP. Your wireless frames carry IPv4 and IPv6 packets identically.
- RFC 8200 — Internet Protocol, Version 6 (IPv6) Specification -- The current IPv6 spec, published in 2017. Supersedes the original RFC 2460 from 1998. Surprisingly readable for a protocol specification.
- RFC 8305 — Happy Eyeballs Version 2 -- The algorithm that makes dual-stack work without users noticing. Short, well-written, and the name alone makes it worth reading.
- Google IPv6 Statistics -- Real-time IPv6 adoption data from Google's perspective. The graph climbing slowly but steadily for 15 years tells the whole story.
- test-ipv6.com -- Run this right now. It'll tell you whether your current connection supports IPv6 and score your readiness. Takes 10 seconds.
I'm writing a book about what makes developers irreplaceable in the age of AI. Join the early access list →
Naz Quadri has tried to read an IPv6 address aloud on a conference call and will never do it again. He blogs at nazquadri.dev. Rabbit holes all the way down 🐇🕳️.