Why not just \d{1,3}(\.\d{1,3}){3}?
Because that shorter, more common pattern accepts 999.999.999.999,
256.1.1.1 and 300.0.0.1 — none of them a valid IPv4 address,
since every octet is an 8-bit number and must be between 0 and 255 inclusive.
“One to three digits” is a much weaker constraint than “0 to 255”,
and the gap between them is exactly the kind of thing that passes a quick manual test
(192.168.1.1 works fine) and then quietly accepts garbage in production. The
pattern on this page is longer specifically to close that gap.
Token by token
The whole pattern is one octet-matching group, reused four times. The octet group itself is four alternatives, tried in order, each covering one slice of 0–255:
| Alternative | Covers |
|---|---|
25[0-5] | 250–255 |
2[0-4][0-9] | 200–249 |
1[0-9]{2} | 100–199 |
[1-9]?[0-9] | 0–99, with no leading zero required or permitted for the single-digit case |
Alternation in JavaScript regex tries each branch left to right and takes the first one
that matches, so ordering matters here: 25[0-5] must come before
2[0-4][0-9], or a number like 253 would never reach the branch that
can actually match it, because a looser branch tried first can consume the wrong number of
characters and leave the rest of the pattern unable to continue. (In this specific case the
branches are mutually exclusive by their leading digits, so the order is for correctness of
intent more than a live bug — but it is the right habit for alternation in general.)
The full octet group is wrapped as (…) and then reused:
(\.(…)){3} repeats “a literal dot, then another octet” exactly
three times, giving four octets in total — one at the start, three more after a
dot each.
What it matches
192.168.1.1— a private-range address, still a syntactically valid one.0.0.0.0and255.255.255.255— both boundary values of the valid range, both accepted.8.8.8.8— an ordinary public address.
What it deliberately does not match, and why
| Input | Verdict | Why |
|---|---|---|
256.1.1.1 | Rejected | 256 is one past the maximum octet value of 255 — this is the whole reason the pattern is this long instead of a simple \d{1,3} |
1.2.3 | Rejected | Only three octets, not four |
999.999.999.999 | Rejected | Every octet is out of range |
192.168.1.1.1 | Rejected | Five octets, not four — the anchors mean a fifth trailing group breaks the match entirely rather than being silently ignored |
192.168.001.1 | Accepted | Leading zeros are not rejected by this pattern. Historically, some systems (notably many implementations of the C standard library’s inet_aton) treat a leading-zero octet as octal, so 0100 means decimal 64, not 100 — a discrepancy that has been used in real server-side request forgery (SSRF) bypasses. If your validator needs to guard against this, reject any octet with a leading zero unless it is exactly "0" |
What “valid IPv4 syntax” does not tell you
This pattern answers exactly one question: is this four dot-separated numbers, each in
range? It says nothing about whether the address is routable on the public
internet (many ranges, like 10.0.0.0/8, 172.16.0.0/12 and
192.168.0.0/16, are reserved for private networks and never routed publicly),
whether it is a loopback address (127.0.0.0/8), whether it is
currently assigned to anything, or whether a host actually
responds at that address. Classifying an address into public, private,
loopback, link-local or multicast requires comparing the parsed number against known
ranges — a lookup table, not a regex — and confirming reachability requires an
actual network request (a ping, a socket connection), which this client-side pattern
deliberately does not attempt.
Flavour: this is JavaScript’s regex
This pattern targets JavaScript’s built-in
RegExp,
per ECMA-262.
It uses only literals, character classes, {n} quantifiers and alternation
— every one of those works identically in PCRE and Python’s re, so
this exact pattern (including the {2} quantifier syntax) needs no changes to
move between them. See the cookbook’s flavour-honesty
section for constructs that do not port cleanly.
Worked examples
1. Distinguishing IPv4 from IPv6 in a mixed log
A server log recording client addresses may contain both
203.0.113.4 and 2001:db8::1 on different lines. This pattern
matches only the first shape — IPv6 addresses use colons and hexadecimal groups
entirely unlike this pattern’s dot-and-decimal structure, so they simply do not
match, which is the correct behaviour if you specifically need to separate the two rather
than validate either.
2. Redacting internal IPs before sharing a log externally
With g on and the Replace tab, this pattern finds every IPv4-shaped
string in a block of text. Combined with a check against the private ranges mentioned
above (done separately, in code, after matching), you can redact only internal addresses
and leave public ones visible — the regex finds candidates, the range check decides
which ones to act on.
3. Why 192.168.001.001 is a bug waiting to happen
Two systems that both accept leading-zero octets can still disagree about what the
address means: one may pass it straight to a system call that interprets
010 as octal 8, decimal 8, while another treats it as literal decimal 10.
The mismatch has been used to make an address that looks like it points to a public host
pass a validator and then actually resolve to an internal one when handed to the
operating system’s network stack. If your validator’s job includes security
(blocking access to internal addresses, for instance), reject leading zeros explicitly
rather than relying on this pattern’s permissiveness.
4. Validating a CIDR range, not just a bare address
A firewall rule or an allowlist entry is often written as 192.168.1.0/24
rather than a bare address — an IPv4 address, a literal slash, then a prefix length
from 0 to 32. This pattern covers only the address portion. Extending it means adding
(?:\/(?:3[0-2]|[12]?[0-9]))? after the existing pattern, as an optional
suffix — itself a small range-correct group in the same style as the octet groups
above, since a prefix length also has a hard numeric ceiling (32) that a loose
\d{1,2} would not enforce.
Using this pattern in your own code
Every construct here — character classes, alternation, {n} quantifiers
— is standard across mainstream regex engines, so the pattern text itself does not
change between a browser, a server script, or the shell.
// JavaScript
const IPV4_RE = /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/;
IPV4_RE.test('192.168.1.1'); // true
IPV4_RE.test('256.1.1.1'); // false
# Python's re module — identical pattern text
import re
IPV4_RE = re.compile(
r'^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'
r'(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$'
)
bool(IPV4_RE.match('8.8.8.8')) # True
# For real work, most languages also ship a dedicated address parser that is
# a better fit than any regex once you need to reason about ranges:
python3 -c "import ipaddress; print(ipaddress.ip_address('192.168.1.1').is_private)"
# -> True
That last line is worth taking seriously as advice, not just a footnote: once the actual
question is “is this address private, loopback, or otherwise special” rather
than “is this text shaped like four octets”, a real IP-address library (Python’s
ipaddress module, Node’s net module, or equivalents in other
languages) already knows every reserved range correctly and will not need to be manually
kept in sync with future allocations the way a hand-written regex would.
Common mistakes when adapting this pattern
- Reordering the alternatives inside the octet group. The order
25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]is deliberate: each branch is tried left to right, and putting a broader branch like[1-9]?[0-9]first would still work correctly here only because the branches are each disambiguated by their leading digit — but as a general habit with alternation, more specific branches belong before more general ones, since a general branch tried first can consume characters a later, more specific branch needed. - Assuming this pattern also validates a subnet mask or CIDR suffix.
192.168.1.1/24fails this pattern outright, because of the trailing$anchor right after the fourth octet. See worked example 4 above for the extension that adds an optional CIDR suffix, rather than assuming the base pattern already handles it. - Trying to also validate IPv6 by adding colons to the character classes.
IPv6’s address grammar (hexadecimal groups, the
::zero-compression shorthand, embedded IPv4 tails in some notations) is different enough from IPv4’s that no small edit to this pattern gets there — it needs its own pattern, built around IPv6’s own rules, not a patched version of this one. - Forgetting that whitespace inside the address is never valid. A
pasted address sometimes carries a stray space from copy-paste —
192.168. 1.1— and the anchored pattern correctly rejects it rather than silently trimming it. Callvalue.trim()and, if needed, strip internal whitespace explicitly before testing, rather than loosening the pattern itself to tolerate spaces that should not be there in the first place.
Frequently asked questions
Does this match IPv6 addresses too?
No. IPv6 has an entirely different textual format — eight
groups of hexadecimal digits separated by colons, with a :: shorthand for
runs of zeros — and needs its own pattern, considerably more involved than this
one, to handle that shorthand correctly.
Why does 192.168.001.1 pass, if leading zeros are a known problem?
Because rejecting them is a deliberate additional constraint, not
a default one, and different applications want different behaviour here. See the table
above for the specific SSRF-adjacent risk, and add
(?!0\d) as a negative lookahead before each octet group if you need to reject
them.
Does a match mean the address is routable on the internet?
No. Routability depends on whether the address falls in a reserved private, loopback, link-local or multicast range, which this pattern does not check. See “what ‘valid IPv4 syntax’ does not tell you” above for the full list of ranges to check separately if that distinction matters to you.
Can I capture each octet separately?
The pattern as written only captures the last octet distinctly
per repetition, because the octet group is nested inside the repeated
(\.(…)){3} structure — JavaScript keeps only the final iteration’s
capture for a group inside a quantified group. To capture all four separately, write out
four named or numbered groups explicitly rather than reusing one group with a
quantifier.
Why 0–255 and not 1–255, since 0.0.0.0 seems unusual?
0.0.0.0 is a valid IPv4 address with defined special
meanings (a default route, or “this host”, depending on context), so it is
correctly included in the syntactic range even though it is unusual to see as a
destination address in practice.
Why do I see four separate matches for one address in the highlighted view?
You should not, for a single address on its own line — the
whole thing (192.168.1.1) is one match, with the repeated octet group
contributing to the count internally but not producing separate top-level matches. If a
test string has several addresses on separate lines, each line produces its own match,
which is the g flag doing exactly what it is meant to: find every
occurrence, not just the first.
Is my IP address sent anywhere when I test it here?
No. Matching runs inside a same-origin Web Worker, and there is no upload endpoint anywhere on this page — open the Network panel in developer tools and watch it stay empty while you paste addresses in.
Limitations
- IPv4 only — no IPv6 support.
- Does not reject leading zeros, which some systems interpret as octal; add
(?!0\d)per octet if that matters for your use case. - Does not classify the address as private, loopback, link-local or public — that needs a separate range check against the parsed numeric value.
- Cannot confirm reachability or that anything is actually listening at the address — that needs a real network request, out of scope for a client-side pattern.
Related
- Regex to match a URLFor when the host in a link is a bare IP rather than a domain
- Regex for phone numbersAnother format built from digit groups with a fixed, checkable shape
- Regex for an ISO 8601 dateRange-correct digit matching applied to a calendar instead of an address
- About this toolWhy it exists and how the tester is built
- JSON formatterAnother text tool that runs entirely in your browser
- All toolsSee everything on this site