IP Address Validation with Regex
IP address validation looks straightforward — four numbers separated by dots — until you realise 999.999.999.999 matches the naive pattern /(\d+\.){3}\d+/ while being a completely invalid address. Real validation requires each octet to be constrained to 0–255. The correct IPv4 pattern is longer but bulletproof.
The Strict IPv4 Pattern
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/Broken down — the octet alternative (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):
25[0-5]— matches 250–2552[0-4][0-9]— matches 200–249[01]?[0-9][0-9]?— matches 0–199 (optional leading 0 or 1, then 1–2 digits)
That three-way alternation is repeated four times (three with trailing dots, one without) to build a full address. Any octet outside 0–255 fails to match.
Simpler (Weaker) Patterns
/^(\d{1,3}\.){3}\d{1,3}$/ // accepts 999.999.999.999 — BAD
/^(\d+\.){3}\d+$/ // no length limit — VERY BAD
/^([0-9]{1,3}\.){3}[0-9]{1,3}$/ // same as first, just verboseUse these only when you know the input already comes from a trusted source (like a validated server config) and you just need a rough shape check.
IPv6 Pattern
IPv6 has too many valid representations for a single regex to catch all correctly. The most practical patterns cover the two common forms:
// Full 8-group form: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
/^(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$/
// Compressed with :: (2001:db8::1, ::1, ::)
// This is imperfect — for real validation use a library
/^(?:[A-Fa-f0-9]{1,4}:){1,7}:$|^:(?::[A-Fa-f0-9]{1,4}){1,7}$/For serious IPv6 handling always defer to a real library — net.isIPv6() in Node, ipaddress.IPv6Address() in Python, or InetAddresses.forString() in Java. Regex-based validation misses the IPv4-mapped form ::ffff:192.168.1.1 and zone identifiers like fe80::1%eth0.
Language-Specific Usage
JavaScript
const IPV4_RE = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
function isIPv4(str) {
return IPV4_RE.test(str);
}
isIPv4('192.168.1.1'); // true
isIPv4('255.255.255.255'); // true
isIPv4('256.1.1.1'); // false
isIPv4('192.168.1'); // false
// Node built-in — also handles IPv6
import net from 'node:net';
net.isIP('192.168.1.1'); // 4
net.isIP('::1'); // 6
net.isIP('nope'); // 0Python
import re
import ipaddress
IPV4_RE = re.compile(
r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
)
def is_ipv4(s: str) -> bool:
return bool(IPV4_RE.match(s))
# Preferred: use stdlib ipaddress module
def is_valid_ip(s: str) -> bool:
try:
ipaddress.ip_address(s)
return True
except ValueError:
return FalsePHP
function isIPv4(string $s): bool {
$p = '/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
. '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
return (bool) preg_match($p, $s);
}
// PHP also has built-in filter
filter_var('192.168.1.1', FILTER_VALIDATE_IP); // string(11) "192.168.1.1"
filter_var('192.168.1.1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
filter_var('::1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);Special Range Detection
CIDR notation
// IPv4 + /prefix (e.g. 192.168.1.0/24)
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(\d|[12]\d|3[0-2])$/
// Prefix limited to 0-32: \/(\d|[12]\d|3[0-2])
// \d = 0-9
// [12]\d = 10-29
// 3[0-2] = 30-32Private / RFC 1918 ranges
// Any private IPv4 (10.x, 172.16-31.x, 192.168.x)
/^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/
// Localhost
/^127\./
// Link-local
/^169\.254\./IP Regex Cheatsheet
| Target | Pattern (abbreviated) | Best use |
|---|---|---|
| Strict IPv4 | (25[0-5]|2[0-4]\d|[01]?\d\d?) × 4 | User input validation ✅ |
| Loose IPv4 | /^(\d{1,3}\.){3}\d{1,3}$/ | Log parsing (fast, permissive) |
| IPv4 + CIDR | strict IPv4 + \/(\d|[12]\d|3[0-2]) | Firewall rules |
| Private range | /^(10\.|172\.1[6-9]…|192\.168\.)/ | Detect internal traffic |
Testing Your IP Regex
Use the live Regex Tester above with this test suite:
- Valid IPv4:
0.0.0.0,127.0.0.1,192.168.1.1,255.255.255.255 - Invalid octet:
256.1.1.1,1.1.1.999,-1.1.1.1 - Wrong shape:
192.168.1,192.168.1.1.5,192,168,1,1 - With CIDR:
10.0.0.0/8,192.168.1.0/24,10.0.0.0/33(invalid prefix) - IPv6:
::1,2001:db8::1,fe80::1%eth0
Common Mistakes When Writing IP Regex
IP-address regex is one of the most-written and most-buggy patterns in production code. Watch for these classic pitfalls:
- Naive dot-separated digits. Patterns like
/^\d{1,3}(\.\d{1,3}){3}$/accept invalid octets such as999.999.999.999. Enforce the per-octet 0–255 range with alternation:(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d). - Missing leading-zero rejection. Some parsers interpret
010as octal (value 8), causing subtle bugs. If your storage layer is strict, reject leading zeros explicitly with a non-capturing alternation. - Confusing IPv4 with IPv6. An IPv6 regex will not match
::ffff:192.168.1.1correctly unless you handle the IPv4-mapped IPv6 form. Consider validating each family separately, or use the language's built-in parser (Python'sipaddress, Node'snet.isIP) for authoritative results. - Ignoring CIDR prefix bounds. A pattern that allows
/33for IPv4 or/129for IPv6 leaks invalid ranges. Enforce/([0-9]|[12][0-9]|3[0-2])for IPv4 and/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8])for IPv6. - Not stripping whitespace. User input frequently contains trailing spaces or surrounding newlines. Trim before regex, or extend the pattern with
\s*at the edges — never assume clean input.
When to Skip Regex Entirely
For production IP validation, most languages ship a battle-tested parser that outperforms any hand-rolled regex on both correctness and performance. Python's ipaddress.ip_address(), Node's net.isIP(), Go's netip.ParseAddr(), and Rust's IpAddr::from_str() all handle the edge cases (zero compression, IPv4-mapped IPv6, zone identifiers) correctly. Reserve regex for extracting IPs from text or for early-exit rejection at the input boundary — then run the strict parser for authoritative validation. This two-stage pattern is faster and safer than any single monolithic regex.