IPv6 for Web Developers
What web developers need to know about IPv6: URLs with brackets, database storage, socket programming, and common bugs to avoid.
If you've been ignoring IPv6 because "it'll be a problem for later," later is now. Mobile networks run IPv6-first. Some users behind NAT64 can only reach you via IPv6. Even localhost on your development machine probably resolves to ::1 before 127.0.0.1.
This guide covers what you actually need to know: handling IPv6 in URLs, storing addresses in databases, socket programming, and the bugs that will bite you if you're not careful.
IPv6 in URLs#
IPv6 addresses contain colons, which conflicts with the port separator in URLs. The solution is square bracket notation:
http://[2001:db8::1]:8080/api/users
https://[2606:2800:220:1:248:1893:25c8:1946]/Without brackets, parsers can't tell where the address ends and the port begins. This applies to:
- HTTP/HTTPS URLs
- WebSocket connections (
ws://[::1]:3000) - Database connection strings
- Any URI scheme
When generating URLs programmatically, wrap IPv6 addresses in brackets. When parsing, strip them before validation or storage.
Database Storage#
The classic mistake is using VARCHAR(15) for IP addresses. That fits IPv4 (maximum 15 characters: 255.255.255.255) but not IPv6.
PostgreSQL has a native INET type that handles both IPv4 and IPv6:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
ip_address INET NOT NULL
);
-- Automatic validation and normalization
INSERT INTO users (ip_address) VALUES ('2001:db8::1');
INSERT INTO users (ip_address) VALUES ('192.0.2.1');The INET type stores addresses efficiently, validates them on insert, and supports network operations like subnet matching with << and >> operators.
MySQL lacks a native type, so use either:
VARBINARY(16)- store the binary representation (16 bytes for IPv6, 4 for IPv4)VARCHAR(45)- store the string representation (max length of expanded IPv6)
Binary storage is more efficient but requires conversion functions:
-- Store
INSERT INTO users (ip_address) VALUES (INET6_ATON('2001:db8::1'));
-- Retrieve
SELECT INET6_NTOA(ip_address) FROM users;Address Normalization
IPv6 addresses have multiple valid representations. Always normalize before comparing or indexing to ensure consistency.
Normalization matters. IPv6 addresses have multiple valid representations:
2001:0db8:0000:0000:0000:0000:0000:00012001:db8::1(compressed)2001:db8:0:0:0:0:0:1(partially compressed)
Always normalize before comparing or indexing. Most libraries have a canonical form function.
Socket Programming#
The socket API treats IPv4 and IPv6 as separate address families. Modern applications should support both.
Node.js defaults to dual-stack sockets:
const http = require('http');
// Binds to :: (all IPv6 addresses) and accepts IPv4 via mapping
const server = http.createServer((req, res) => {
res.end(`Your IP: ${req.socket.remoteAddress}`);
});
server.listen(3000, '::');The :: address is the IPv6 equivalent of 0.0.0.0, accepting connections on all interfaces. Most systems support IPv4-mapped IPv6 addresses (::ffff:192.0.2.1), allowing a single IPv6 socket to handle both protocols.
Python requires explicit dual-stack handling:
import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.bind(('::', 8080))
sock.listen(5)The IPV6_V6ONLY flag controls whether the socket accepts only IPv6 or both IPv4 and IPv6.
Go makes dual-stack the default:
listener, err := net.Listen("tcp", ":8080")
// Automatically listens on both IPv4 and IPv6For client connections, always use getaddrinfo() (or your language's equivalent) instead of manually resolving addresses. It handles IPv4, IPv6, and dual-stack scenarios correctly:
import socket
# Don't do this
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))This hardcodes IPv4 and fails on IPv6-only networks.
# Do this - works for both IPv4 and IPv6
addr_info = socket.getaddrinfo('example.com', 80, socket.AF_UNSPEC, socket.SOCK_STREAM)
sock = socket.socket(addr_info[0][0], addr_info[0][1])
sock.connect(addr_info[0][4])Local Testing#
Most systems resolve localhost to both ::1 (IPv6) and 127.0.0.1 (IPv4). Check your /etc/hosts:
127.0.0.1 localhost
::1 localhostIf your application binds only to 127.0.0.1, it won't accept IPv6 connections. Bind to :: for dual-stack or explicitly bind to both addresses.
To test IPv6-only behavior, disable IPv4 on your loopback interface or use [::1] explicitly in your HTTP client:
curl http://[::1]:3000/For realistic testing, use an IPv6-only test network. Many cloud providers offer IPv6-only instances that force you to handle edge cases like NAT64/DNS64.
CDN and Origin Servers#
Most CDNs (Cloudflare, Fastly, AWS CloudFront) support IPv6 by default. They'll serve content over IPv6 to clients and translate to IPv4 for your origin if needed.
The catch: if you expose APIs directly (bypassing the CDN), your origin server must support IPv6. Check your DNS has AAAA records and your firewall allows IPv6 traffic.
Common Bugs#
Regex validation. IPv6 regex patterns are notoriously complex and usually wrong:
// Don't do this
const ipv6Regex = /^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}$/i;
// Fails on :: compression, IPv4-mapped addresses, zone IDs...Use a proper parsing library instead. Every major language has one that handles IPv6 correctly. For validation, use our IPv6 Validator tool.
Hardcoded IPv4 addresses. Search your codebase for patterns like:
const API_SERVER = 'http://192.168.1.100:3000'; // Won't work with IPv6Use hostnames instead, or support both address families.
IP-based rate limiting. IPv6 addresses change frequently due to privacy extensions. Rate limiting by exact address will fail. Limit by /64 subnet instead:
// Bad
const key = `rate:${ipAddress}`;
// Better for IPv6
const key = `rate:${ipv6ToSubnet64(ipAddress)}`;Privacy Extensions Impact
IPv6 privacy extensions change addresses frequently, breaking exact-match rate limiting. Always use subnet-based limits for IPv6.
Libraries without IPv6 support. Older HTTP clients, database drivers, and networking libraries may not handle IPv6. Test with actual IPv6 addresses, not just localhost. If a library fails, check for updates or alternatives.
URL parsing edge cases. Ensure your router/framework handles bracketed IPv6 addresses:
GET http://[2001:db8::1]:8080/api/users
Host: [2001:db8::1]:8080Some parsers incorrectly include the brackets in the Host header or fail to extract the address correctly.
Checklist#
Your application is IPv6-ready when:
- Database columns can store 45-character strings or use native IP types
- Servers bind to
::or explicitly listen on both IPv4 and IPv6 - Client code uses
getaddrinfo()or equivalent for name resolution - IP validation doesn't rely on regex
- No hardcoded IPv4 addresses in configuration
- Rate limiting and geolocation handle IPv6 subnets
- URL parsing handles bracketed notation
IPv6 isn't optional anymore. Build it in from the start, and you'll avoid the painful migration later.
Related Articles#
- IPv6 Fundamentals - Understanding IPv6 addressing and core concepts
- IPv6 Address Types - Learn the different types of IPv6 addresses your code will encounter
Validate IPv6 Addresses
Use our IPv6 Validator and Ping Tool to test your application's IPv6 support.