The practical IPv4 regex with octet bounds: ^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$. It rejects 999.999.999.999 (which a naive \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} would match) by encoding the 0-255 range per octet. The full IPv6 pattern is much longer and supports the compressed :: form, IPv4-mapped addresses like ::ffff:192.0.2.1, and zone identifiers. Below I walk both with code in JavaScript, Python, and PHP, engine notes per language, common mistakes, and call out when to skip regex entirely and use the language's built-in IP parser.
The reason IP regex is famously tricky is that IPv6 has eight different shorthand forms for the same address. The full pattern has to handle every one.
Quick reference
Bounded IPv4, ready to paste:
^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$
Lazy IPv4 (use only for log scraping, then validate ranges in code):
^(\d{1,3}\.){3}\d{1,3}$
IPv4 with CIDR:
^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(?:3[0-2]|[12]?[0-9])$
IPv4: the bounded pattern (rejects 999.999.999.999)
^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$
The single-octet sub-pattern reads as: 25[0-5] (250-255) or 2[0-4][0-9] (200-249) or 1[0-9]{2} (100-199) or [1-9]?[0-9] (0-99). This guarantees every octet is 0-255. Wrap in {3} for the first three octets and then one more without the trailing dot.
What this rejects:
999.0.0.1, because the last octet pattern requires 0-255.1.2.3, because only three groups appear; the regex requires four.1.2.3.4.5, five groups.01.02.03.04. Strictly speaking the[1-9]?[0-9]branch allows leading zeros only at the small numbers (0-9). A leading-zero010would fail because010does not match any of the four branches cleanly.
The lazy version most people write: ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$. It is simpler and accepts garbage like 999.999.999.999. Use it only for log scanning where you'll do range validation in a second step.
IPv6: the full pattern (with compression)
IPv6 addresses are eight 16-bit groups in hex, separated by colons. The shorthand allows leading zeros to drop and consecutive zero-groups to collapse to :: (used once per address).
^(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}$|^(?:[0-9A-Fa-f]{1,4}:){1,7}:|^(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}$|^(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}$|^(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}$|^(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}$|^(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}$|^[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})$|^:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:)$|^fe80:(?:[0-9A-Fa-f]{0,4}:){0,4}%[0-9a-zA-Z]+$|^::(?:[Ff]{4}(?::0{1,4})?:)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$|^(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$
Yes, it is that long. The pattern is a union of every legal IPv6 form: full 8-group, various compressed forms, link-local with zone identifier (fe80::1%eth0), IPv4-mapped (::ffff:192.0.2.1), and IPv4-translated.
In practice, almost nobody writes this regex by hand. Use the parser (next section) unless you absolutely need it.
Combined IPv4 or IPv6
For "is this any IP address", alternate the two patterns:
^(IPv4-pattern)|(IPv6-pattern)$
In code, this is usually two separate test() calls (one per pattern) rather than one giant union, for readability.
CIDR (network prefix length)
CIDR notation is an IP followed by a slash and a prefix length: 192.0.2.0/24 or 2001:db8::/32. To match:
IPv4 CIDR: ^(IPv4-pattern)\/(?:3[0-2]|[12]?[0-9])$
IPv6 CIDR: ^(IPv6-pattern)\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$
The prefix length is 0-32 for IPv4 and 0-128 for IPv6.
Examples in JavaScript, Python, and PHP
JavaScript:
const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/;
function isValidIPv4(input) {
return ipv4.test(input);
}
isValidIPv4("192.0.2.1"); // true
isValidIPv4("999.0.0.1"); // false
isValidIPv4("192.0.2"); // falsePython:
import re
IPV4 = re.compile(
r"^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}"
r"(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$"
)
def is_valid_ipv4(value: str) -> bool:
return bool(IPV4.match(value))PHP:
function isValidIPv4(string $value): bool {
$pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/';
return (bool) preg_match($pattern, $value);
}When to skip regex and use the IP parser
For anything more than a quick format check, use the language's built-in IP parser. They handle every shorthand, IPv4-mapped, and zone-identifier case correctly without you having to maintain the long regex.
JavaScript (Node.js):
const net = require("node:net");
net.isIP("192.0.2.1"); // 4
net.isIP("2001:db8::1"); // 6
net.isIP("not.an.ip"); // 0Python:
import ipaddress
try:
ipaddress.ip_address("2001:db8::1") # works for both v4 and v6
valid = True
except ValueError:
valid = FalsePHP:
$isV4 = filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
$isV6 = filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
$isAny = filter_var($value, FILTER_VALIDATE_IP);The parser route is dramatically simpler and more correct. The regex route is for when you can't easily call a parser (logs being scanned by grep, validation rules in a config file that only accepts patterns, etc.).
Engine compatibility
The bounded IPv4 pattern is universal. The IPv6 pattern uses alternation and non-capturing groups, which all major engines support. The parser is the better option when available.
| Engine | Bounded IPv4 | Full IPv6 | Parser fallback |
|---|---|---|---|
| JavaScript | Works | Works | net.isIP (Node) returns 4 / 6 / 0 |
| Python | Works | Works | ipaddress.ip_address, ipaddress.ip_network for CIDR |
| PHP (PCRE) | Works | Works | filter_var($v, FILTER_VALIDATE_IP) with FILTER_FLAG_IPV4 / FILTER_FLAG_IPV6 |
| Java | Works | Works | InetAddress.getByName (which also does DNS, so guard with isInetAddress check first) |
| .NET | Works | Works | IPAddress.TryParse(s, out _) |
| Go (RE2) | Works | Works | net.ParseIP(s) returns nil for invalid input |
Rust (regex crate) | Works | Works | std::net::IpAddr::from_str |
| Ruby | Works | Works | IPAddr.new(s) raises on invalid |
POSIX ERE (grep -E) | Works (replace \d with [0-9]) | Works | None; combine with shell logic |
For Rust and Go, where the IPv6 regex is annoyingly long, I always use the parser and only fall back to regex when scanning streams.
Common mistakes
The bugs I've shipped or seen in code review.
Using \d{1,3} per octet and calling it done. This matches 999.999.999.999, 0.0.0.0, and every other "four numbers separated by dots" but says nothing about whether each number is in range. Use the bounded form for validation; the lazy form is acceptable for log scraping only.
Forgetting the anchors. Without ^ and $, the IPv4 pattern matches 99.999.0.0.1 because the engine finds a valid IP substring at position 1. Always anchor for validation.
Accepting leading zeros silently. 010.020.030.040 looks valid but most parsers treat octets with leading zeros as octal, which changes the actual address. The bounded pattern in this article rejects leading-zero forms because the [1-9]?[0-9] branch only allows 0 by itself.
Treating the IPv6 zone identifier as part of the address. fe80::1%eth0 is a valid link-local address, but the %eth0 part is local context, not a network-routable suffix. If you store the IP for later use, strip the zone identifier first.
Trying to handle CIDR in the same pattern as the address. It gets ugly fast. Match the address and the prefix separately, or split on / first and validate the two halves independently.
Forgetting that :: can appear at most once. 2001::db8::1 is invalid because the regex can't tell which group of zeros each :: expands to. Real parsers reject this; a sloppy regex might accept it.
Test cases
| Input | IPv4 | IPv6 |
|---|---|---|
192.0.2.1 | Match | No match |
255.255.255.255 | Match | No match |
0.0.0.0 | Match | No match |
192.0.2 | No match | No match |
999.0.0.1 | No match | No match |
2001:db8::1 | No match | Match |
::1 | No match | Match |
::ffff:192.0.2.1 | No match | Match (IPv4-mapped) |
fe80::1%eth0 | No match | Match (zone) |
not.an.ip | No match | No match |
FAQ
See also
- How to Match a URL with Regex: the URL case which sometimes contains an IP literal like
http://[2001:db8::1]/ - How to Match a Domain Name with Regex: for inputs that could be either a domain or an IP
- How to Match Numbers with Regex: the port-number cousin pattern often paired with IP fields
- Regex Anchors: why
^and$are non-negotiable for IP validation - Regex Lookaheads and Lookbehinds: composing extra constraints on top of the IP pattern
- Regex Capturing Groups and Backreferences: pull individual octets or hex groups out of the match
- Regex Cheat Sheet: the wider syntax and engine compatibility reference
External reference: the IETF RFC 4291 defines IPv6 addressing. For interactive testing of these patterns, paste into regex101.com.





