IPv4 vs IPv6 in 2026 — what every developer should know
For most of the last decade, IPv6 felt like a topic you could safely ignore. "Still mostly IPv4 out there" was a reasonable excuse. That excuse stopped being true in 2026.
In March 2026, native IPv6 access to Google exceeded 50% globally for the first time in the 18 years Google has tracked this data. Around the same time, IPv6 overtook IPv4 as the majority protocol for global internet traffic as a whole. If you're building anything that touches the network — a backend service, a mobile app, an IoT device — this is no longer a future problem. It's a today problem.
This article covers what changed, why it matters for how you write code, and practical examples for detecting and handling both protocols correctly.
Why this happened now
IPv6 has existed since 1998. The slow adoption for most of its life wasn't a technical failure — it was an economic one. IPv4 addresses, while exhausted, kept getting recycled, traded, and NAT'd around for years longer than anyone expected. Migrating an entire network stack costs money and carries risk; as long as IPv4 kept limping along, there was no urgent reason to switch.
Three forces changed the calculus:
Mobile carriers went IPv6-first. Akamai data shows 88.4% of T-Mobile's network running on IPv6, with AT&T at 74% and Verizon at 74.8%. Most phone users today are on IPv6 without knowing it.
IoT made IPv4 impractical at scale. The number of IoT devices grew from roughly 15 billion in 2015 to over 75 billion in 2025, and these devices increasingly default to IPv6 because it removes the need to manage NAT rules and port forwarding for every single device.
Adoption is wildly uneven by country. France sits at 73% IPv6 adoption, India at 72%, Saudi Arabia at 65% — while Italy is at 17%, Spain at 10%, and Egypt at 4%. If your users are concentrated in IPv6-heavy regions, you're already serving majority-IPv6 traffic whether you've planned for it or not.
What actually changes for you as a developer
IPv6 isn't "IPv4 but longer." A few practical differences matter immediately:
No more NAT assumptions. With IPv4, most consumer devices sit behind NAT, so the IP you see is shared across a household or office. With IPv6, every device commonly gets its own globally routable address. This changes how you think about rate limiting, abuse detection, and geolocation — one IP no longer reliably maps to "one network", it often maps to "one device."
Address format is different everywhere. 2001:db8::1 instead of 203.0.113.42. This breaks naive regex validation, log parsers, and URL construction (IPv6 literals in URLs need brackets: https://[2001:db8::1]:8080/).
Dual-stack is the norm, not the exception. Most networks today run both protocols simultaneously. Your server, your client libraries, and your monitoring all need to handle a connection coming in on either protocol gracefully — not as an edge case.
Detecting IPv4 vs IPv6 in your stack
Here's how to handle both protocols correctly across common languages and frameworks.
JavaScript / Node.js
function getIPVersion(ip) {
if (ip.includes(':')) return 'IPv6';
if (ip.includes('.')) return 'IPv4';
return 'unknown';
}
// Express middleware to log both correctly
app.use((req, res, next) => {
const ip = req.ip; // may be IPv4 or IPv6 depending on connection
console.log(`${getIPVersion(ip)} request from ${ip}`);
next();
});
A common bug: Express and many proxies report IPv4 addresses as IPv6-mapped, like ::ffff:203.0.113.42. Strip the prefix before doing anything with it:
function normalizeIP(ip) {
if (ip.startsWith('::ffff:')) {
return ip.substring(7); // back to plain IPv4
}
return ip;
}
Python
import ipaddress
def get_ip_version(ip: str) -> str:
try:
addr = ipaddress.ip_address(ip)
return f"IPv{addr.version}"
except ValueError:
return "invalid"
print(get_ip_version("203.0.113.42")) # IPv4
print(get_ip_version("2001:db8::1")) # IPv6
ipaddress is in the standard library and handles validation, version detection, and even subnet math — no need for regex.
Go
package main
import (
"fmt"
"net"
)
func getIPVersion(ipStr string) string {
ip := net.ParseIP(ipStr)
if ip == nil {
return "invalid"
}
if ip.To4() != nil {
return "IPv4"
}
return "IPv6"
}
func main() {
fmt.Println(getIPVersion("203.0.113.42")) // IPv4
fmt.Println(getIPVersion("2001:db8::1")) // IPv6
}
PHP
function getIPVersion(string $ip): string {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return 'IPv4';
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return 'IPv6';
}
return 'invalid';
}
Detecting client IP and protocol from the browser
If you need to know whether your visitor is reaching you over IPv4 or IPv6 (useful for diagnostics, dual-stack debugging, or simply curiosity), the cleanest approach is hitting protocol-specific endpoints — not parsing user agent strings or making assumptions.
IPPubblico.org exposes dedicated subdomains for exactly this:
// Forces an IPv4-only connection and returns the IPv4 address
fetch('https://ipv4.ippubblico.org/')
.then(res => res.text())
.then(ip => console.log('IPv4:', ip.trim()));
// Forces an IPv6-only connection — fails gracefully if the client has no IPv6
fetch('https://ipv6.ippubblico.org/')
.then(res => res.text())
.then(ip => console.log('IPv6:', ip.trim()))
.catch(() => console.log('No IPv6 connectivity'));
Or get both in one call:
fetch('https://ippubblico.org/?text=1')
.then(res => res.text())
.then(text => {
const lines = text.trim().split('\n');
console.log(lines[0]); // IPv4: x.x.x.x or NONE
console.log(lines[1]); // IPv6: x or NONE
});
This pattern is genuinely useful: if ipv6.ippubblico.org times out or fails, you know definitively that the client has no IPv6 path to the public internet — not just that your app didn't request it.
Common pitfalls
Storing IPs as 32-bit integers. A classic IPv4-era optimization that breaks completely with IPv6. If your schema has an INT UNSIGNED column for IP storage, it cannot hold an IPv6 address. Use VARCHAR(45) or a native INET/CIDR type if your database supports one (PostgreSQL does).
Regex-based IP validation. Most handwritten IPv4 regexes don't even attempt IPv6, and IPv6's flexible notation (leading zero compression, :: shorthand, embedded IPv4) makes a correct regex genuinely hard to write by hand. Use your language's built-in IP parsing library instead — ipaddress in Python, net.ParseIP in Go, inet_pton in C, FILTER_VALIDATE_IP in PHP.
Rate limiting by IP alone. Since IPv6 commonly gives each device its own address while IPv4 often shares one address across many users behind NAT, identical rate-limiting logic produces very different real-world behavior depending on which protocol a request arrives on. A single aggressive IPv6 client looks like one bad actor; a misbehaving app on a shared IPv4 NAT can look like — and inadvertently punish — hundreds of innocent users at once.
CIDR notation differences. IPv4 subnets you're used to (/24, /16) map to wildly different scales in IPv6, where /64 is the standard allocation for a single network segment — already larger than the entire IPv4 address space.
Should you do anything about this right now?
If your stack already uses standard library IP parsing and your database can store variable-length strings, you're probably fine technically — dual-stack support is mostly already there in modern frameworks by default.
What's worth auditing:
- Any handwritten IP validation or parsing logic
- Database columns storing IPs as fixed-size integers
- Rate limiting or abuse detection logic that assumes IPv4-style address sharing
- Log analysis tools or dashboards that choke on the longer IPv6 format
- Any hardcoded assumption that "an IP" is 4 bytes
None of this requires a rewrite. It requires checking that the assumptions baked into older code still hold — and increasingly, for a majority of your traffic, they don't.
Quick reference
| Task | IPv4 | IPv6 |
|---|---|---|
| Example address | 203.0.113.42 |
2001:db8::1 |
| URL with port | 203.0.113.42:8080 |
[2001:db8::1]:8080 |
| Typical subnet |
/24 (256 addresses) |
/64 (≈1.8×10¹⁹ addresses) |
| NAT behavior | Usually shared across users | Usually one address per device |
| Detect with IPPubblico | https://ipv4.ippubblico.org/ |
https://ipv6.ippubblico.org/ |
Conclusion
IPv6 crossing the 50% threshold in March 2026 is the kind of milestone that's easy to miss because nothing visibly "switched over" — there was no flag day, no announcement, just a slow curve that quietly crossed the midpoint. But for anyone writing code that touches the network, the practical implication is concrete: code that silently assumes IPv4 is now wrong for roughly half of real-world traffic, and that share is still climbing.
The fix isn't dramatic. Use your language's built-in IP libraries, store IPs as strings, and stop assuming one IP equals one user. The protocol your code runs on increasingly isn't your choice to make — it's the network's.
Have you run into IPv6-related bugs in production? Share them in the comments — the edge cases are usually the most instructive part.
Top comments (0)