← Blog

IPv4 vs IPv6: what developers actually need to know

The practical differences you actually hit in code — notation, address space, :: compression, embedded IPv4 — and why every field that takes an IP now has to accept both.

Contents

If you have ever written a regex for an IP address and watched it fall over the first time someone typed ::1, you already understand the practical stakes of IPv4 vs IPv6. This is the developer's version of the comparison — not a history lecture, but the handful of concrete differences that show up in your parsing, your storage, and your validation: two very different notations, a wildly larger address space, the :: shorthand that trips naive code, IPv4 addresses hiding inside IPv6 ones, and the fact that any input field taking an IP today has to accept both.

Two notations for the same job

An IPv4 address is 32 bits, written as four decimal octets from 0 to 255, separated by dots — the familiar dotted-quad:

192.0.2.1

An IPv6 address is 128 bits, written as eight groups of four hexadecimal digits (call them hextets), separated by colons:

2001:0db8:0000:0000:0000:ff00:0042:8329

Same job — identify a host on a network — but almost nothing about the string is portable between them. IPv4 is decimal and dot-separated; IPv6 is hexadecimal, colon-separated, and case-insensitive (canonical form is lowercase). Code that assumes "an IP has three dots in it" is already wrong for half the internet.

Why IPv6 exists: the address space

The reason for the second format is arithmetic. IPv4's 32 bits give you 2^32 addresses — about 4.3 billion. That sounded infinite in 1981 and ran out in practice years ago; the workarounds (NAT, carrier-grade NAT) are why your phone and your laptop can share one public address.

IPv6's 128 bits give you 2^128 addresses. The number is large enough that it stops being intuitive, so the useful takeaway is simply this: the space is why the format changed, and why the format changed is why your code has to handle both for the foreseeable future. (We are not going to quote you an adoption percentage — pick your own number from your own traffic logs, not a blog post.)

:: and leading zeros: the compression rules

IPv6 addresses are long, so the spec defines two shortenings, and both matter when you compare or store addresses.

First, drop leading zeros within each hextet: 0db8 becomes db8, 0000 becomes 0.

Second, replace one run of consecutive all-zero hextets with a double colon ::. You may do this only once per address — two :: would be ambiguous, because a parser couldn't tell how many zero groups each one stood for.

Apply both rules and the address above collapses:

2001:0db8:0000:0000:0000:ff00:0042:8329
2001:db8::ff00:42:8329

Both strings are the same address. So is ::1 (loopback, all zeros but the last bit) and :: (the unspecified address, all zeros). The consequence for your code is direct: string equality is not address equality. 2001:db8::1 and 2001:0db8:0000:0000:0000:0000:0000:0001 are identical hosts and different strings. To compare, you normalize first, then compare — which is exactly what a real parser gives you:

import ipaddress

a = ipaddress.ip_address("2001:db8::1")
b = ipaddress.ip_address("2001:0db8:0000:0000:0000:0000:0000:0001")

a == b               # True — compared as integers, not strings
a.compressed         # '2001:db8::1'  (canonical short form)
a.exploded           # '2001:0db8:0000:0000:0000:0000:0000:0001'

Embedded IPv4, and the parsers that trip on it

The two worlds also overlap on purpose. An IPv4-mapped IPv6 address embeds a dotted-quad inside IPv6 notation, which is how dual-stack sockets represent an IPv4 peer:

::ffff:192.0.2.1

There are others — NAT64 uses the 64:ff9b::/96 prefix to carry IPv4 addresses across an IPv6-only network — but the point for a developer is the same. A string with both colons and dots is a valid, common address, and a hand-rolled splitter that keys on one separator or the other will mangle it. This is the single strongest argument for never validating IPs with a regex: the format has legal forms your pattern's author never imagined. Reach for the parser your language already ships.

Dual-stack: accept both, everywhere

Because both protocols are live, every place you accept an IP — a signup form, an X-Forwarded-For header, an allowlist — receives both. Node's standard library makes the accept-both check a one-liner, and it returns which family it found:

const net = require("node:net");

net.isIP("192.0.2.1");        // 4
net.isIP("2001:db8::ff00:42:8329"); // 6
net.isIP("::ffff:192.0.2.1"); // 6  (embedded IPv4 is still IPv6)
net.isIP("not-an-ip");        // 0  (falsy = invalid)

For most fields, that is genuinely the whole job, and if the standard library answers your question you should not pay anyone for more. We cover the language-specific path in validating an IP address in Node.js, and the classification ranges — private, loopback, bogon — in what is a private IP address.

Where an API is the layer past the stdlib

The parser tells you a string is a valid address and which version it is. Boundstone does that same work over HTTP, as part of one system, and returns a small classification alongside it — so a service in Go, a worker in PHP, and a webhook in Ruby all get the identical contract instead of three subtly different local answers:

curl -s https://api.boundstone.io/v1/verify/ip \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"2606:4700:4700::1111"}'
{
  "valid": true,
  "version": 6,
  "normalized": "2606:4700:4700::1111",
  "classification": "public",
  "is_public": true,
  "is_bogon": false,
  "checks": {
    "performed": ["format", "version", "range_classification"],
    "not_performed": ["geolocation", "asn", "hosting_datacenter", "proxy_vpn_tor", "reputation"]
  }
}

Note it accepts and classifies both families — hand it a dotted-quad or a colon-separated hextet string like the one above, and version comes back 4 or 6 with normalized in canonical form, so you get the equality-safe string for free.

Read the not_performed list, because it is the honest part of the answer. Boundstone does not geolocate the address, look up its ASN, detect whether it belongs to a hosting or datacenter provider, flag proxy/VPN/Tor, or score its reputation. That IP intelligence needs licensed data and is not shipped — so a valid: true here means exactly "this is a well-formed, classified address," and nothing you didn't ask for. That is the whole point: the list of what we skipped is why you can trust the field we returned. If you just want to paste one in and see it, the keyless IP validator tool runs the same check with no signup.

The short version

  • Notation: IPv4 is four decimal octets with dots; IPv6 is eight hex hextets with colons. Don't assume one shape.
  • :: compresses one run of zero hextets, once. Normalize before you compare — equal addresses are not equal strings.
  • Embedded IPv4 (::ffff:192.0.2.1) is real and common, and stays IPv6. Use a parser, not a regex.
  • Accept both: ipaddress.ip_address in Python, net.isIP in Node do the format-and-version work correctly and for free.
  • Reach for an API when you want one identical contract across languages plus public/private/bogon classification in a single call — and read the not_performed list so you know precisely what a "valid" does and doesn't claim.

Frequently asked questions

What is the difference between IPv4 and IPv6?

IPv4 uses 32-bit addresses written as four decimal numbers between 0 and 255, like 192.168.1.1, which gives about 4.3 billion possible addresses that the internet has effectively exhausted. IPv6 uses 128-bit addresses written as up to eight groups of hexadecimal digits, like 2606:4700:4700::1111, providing a practically unlimited pool. Both still run side by side today, so any code that accepts, stores, or validates IP addresses needs to handle both formats rather than assuming the old four-number shape.

How do I validate whether a string is a real IPv4 or IPv6 address?

You parse the string against the format rules for each version: an IPv4 address must be four octets from 0 to 255, and an IPv6 address must be valid hexadecimal groups, including shorthand like :: for runs of zeros. A good validator also tells you the version and classifies the range, for example whether the address is public, private, loopback, link-local, reserved, or a bogon. Boundstone's IP check does exactly this (format, version, and range classification) and is honest that a valid result only confirms the address is well-formed and what kind of address it is, not who owns it or where it sits.

Can an IP address alone tell me a user's location or whether they are on a VPN?

No. The address itself only reveals whether it is well-formed and what kind of range it falls in, such as public, private, or reserved. Mapping an IP to a city, network operator, or flagging it as a proxy, VPN, Tor exit, or datacenter requires separate licensed geolocation and reputation data, and those lookups can be stale or wrong. Boundstone is deliberate about this line: its IP check reports format, version, and range classification, and explicitly lists geolocation, ASN, hosting/datacenter, proxy/VPN/Tor, and reputation as not performed rather than guessing at them.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist