Validate a Domain Name with Regex in PHP (Tested)

Updated: 4 min read Editorial team The PHP Experts
Short answer

A valid domain name is a series of dot-separated labels, each 1 to 63 characters of a to z, 0 to 9 and hyphens, not starting or ending with a hyphen, at most 253 characters in total. In PHP, check it with a regex that uses a lookahead for the overall length, and convert internationalised names with idn_to_ascii() first.

The first domain regex most people copy is either too strict or too loose. Too strict, and bücher.com or xn--bcher-kva.com fail. Too loose, and -start.com or a 70-character label slip through. Either way it comes back as a bug report.

The rules behind a valid domain name

They come from RFC 1035 and RFC 1123 and fit in a table. For names that are registered and used on the public internet:

RuleValueSource
Characters per labela to z, 0 to 9, hyphenRFC 1035, relaxed by RFC 1123
Label length1 to 63RFC 1035
Total length253 characters max (without trailing dot)RFC 1035, 255 octets on the wire
Hyphennot first or last in a labelRFC 1035
Positions 3 and 4-- reserved for xn-- and future prefixesRFC 5891
CaseinsensitiveRFC 4343

Non-ASCII characters aren’t in the table because they never appear in DNS. bücher.com is xn--bcher-kva.com on the wire. So the sensible approach is to validate the ASCII form and convert first, which the punycode guide covers.

The domain name regex, piece by piece

<?php
declare(strict_types=1);

const DOMAIN_REGEX = '/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{1,59})$/i';

What each part does:

  • (?=.{1,253}$) is a lookahead that checks the total length without consuming characters.
  • [a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])? is one label: alphanumeric at both ends, up to 61 characters including hyphens in between, 63 at most.
  • \. follows each label and + requires at least one, so a bare localhost fails.
  • (?:[a-z]{2,63}|xn--[a-z0-9-]{1,59}) is the TLD: two to 63 letters, or an IDN TLD such as xn--p1ai for .рф.

Rejecting digits in the TLD is stricter than the RFCs, but it matches reality. No delegated TLD is numeric, and allowing example.123 also lets IP addresses through.

A complete validation function

The regex can’t handle Unicode input or the reserved third and fourth positions on its own. A thin wrapper does both:

<?php
declare(strict_types=1);

function isValidDomain(string $input): bool
{
    $domain = rtrim(strtolower(trim($input)), '.');   // tolerate the FQDN trailing dot

    // convert internationalised names before matching
    if (preg_match('/[^\x20-\x7e]/', $domain)) {
        $domain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
        if ($domain === false) {
            return false;
        }
    }
    if (preg_match(DOMAIN_REGEX, $domain) !== 1) {
        return false;
    }
    // hyphens in positions 3 and 4 are reserved for xn-- (RFC 5891)
    foreach (explode('.', $domain) as $label) {
        if (substr($label, 2, 2) === '--' && str_starts_with($label, 'xn--') === false) {
            return false;
        }
    }
    return true;
}

trim() and stripping the trailing dot are choices for user input. Pasted values often carry spaces, and a trailing dot is actually the fully qualified spelling. If you’d rather reject those, remove the two calls.

Regex vs filter_var(): 17 test cases

PHP ships FILTER_VALIDATE_DOMAIN with FILTER_FLAG_HOSTNAME. I ran both against the same inputs on PHP 8.5:

InputisValidDomain()filter_var()
example.comyesyes
sub.example.co.ukyesyes
müller.deyesno
EXAMPLE.COM.yesyes
xn--mller-kva.deyesyes
-start.denono
ende-.denono
a..denono
localhostnoyes
1.2.3.4noyes
ab--cd.denoyes
a.bnoyes
64 × a + .denono
foo_bar.denono
example.123noyes
example.com (padded with spaces)yesno
example.xn--p1aiyesyes

filter_var() validates hostnames, not internet domains. That’s why localhost, 1.2.3.4 and a.b pass. Useful for a server name in a config file, wrong for a “your website” field. It also has no idea about IDNs.

Where a regex stops being useful

A regex tells you whether a string looks like a domain. It can’t tell you whether the TLD exists, whether the name is registered, or where the registrable part begins:

  • Real TLDs are listed in the IANA root zone. If you only accept extensions you support, compare against your own list.
  • Registration status needs a registry or registrar lookup, as in checking domain availability with PHP.
  • Which part of shop.example.co.uk is the actual domain needs the Public Suffix List in PHP.

The classic mistake is stretching one regex over all three, typically with a hard-coded TLD alternation like (com|net|org|info). Nobody updates it, and two years later the form rejects .app and .dev. With more than a thousand delegated TLDs, an allowlist inside a regex is a maintenance trap.

Using the pattern in forms and APIs

Validate shape first, before any DNS lookup or API call. It saves requests and gives instant feedback. For email addresses the domain part follows the same rules; the local part has its own quirks, covered in PHP email validation.

The pattern works unchanged in JavaScript for client-side checks. There’s no idn_to_ascii() in the browser, but new URL('http://' + input).hostname returns the punycode form, so you can convert first and match second. Keep the server-side check regardless; client-side validation is a convenience, not a control.

Got a valid name and want to know whether it’s free? Try it in the betadomain search. For developer projects the .dev extension costs €22.99 a year, and the betadomain domain API answers the same question from code.

Found your domain? You can check and register it directly at compare domain prices – yearly price from €2.99.

FAQ

Is filter_var() with FILTER_VALIDATE_DOMAIN good enough?

For hostnames, yes; for internet domains, not quite. With FILTER_FLAG_HOSTNAME it accepts localhost, 1.2.3.4 and a.b. It also rejects internationalised names like bücher.com unless you convert them to punycode first.

Are underscores allowed in domain names?

Not in hostnames, but DNS itself allows them. Records such as _dmarc.example.com or _sip._tcp.example.com use underscores on purpose. For names people register or type into a browser, leave the underscore out.

Can a regex tell me whether a TLD exists?

No, it only checks shape. Whether a TLD is real is listed in the IANA root zone, and the Public Suffix List adds second-level suffixes like co.uk. For user input, matching against the extensions you actually support is often enough.

Can a domain start with a digit?

Yes. RFC 1123 relaxed RFC 1035's rule that labels must start with a letter, so names like 3com.com or 123.com are valid. Only the TLD itself is never all digits in practice.