PHP has surprisingly good built-in DNS support — better than many developers realize. Five functions cover most DNS tasks without any third-party packages: gethostbyname(), gethostbynamel(), dns_get_record(), checkdnsrr(), and gethostbyaddr(). But PHP's DNS layer also has some real gotchas that can bite you in production: inconsistent error returns, no exception support, and no way to query a specific nameserver.
This guide covers every built-in PHP DNS function, their quirks, patterns for common tasks like email domain validation, and how to reach for Net_DNS2 when the built-in functions are not enough. If you want to understand the protocol fundamentals behind what these functions do, see How DNS Queries Work.
Quick Start: gethostbyname()
The simplest DNS lookup in PHP is a single function call:
$ip = gethostbyname('example.com');
echo $ip; // 93.184.216.34
This resolves a hostname to its first IPv4 address. It uses the operating system's resolver, respects /etc/hosts, and works everywhere PHP runs without any extensions or dependencies.
The gotcha: If the lookup fails, gethostbyname() returns the hostname string, not false or null. This is one of PHP's oldest design quirks, and it trips up developers constantly. Always check:
$ip = gethostbyname('example.com');
if ($ip === 'example.com') {
echo "DNS lookup failed";
} else {
echo "IP: $ip";
}
If you need all A records for a domain (not just the first), use gethostbynamel():
$ips = gethostbynamel('google.com');
// ["142.250.80.46", "142.250.80.78", ...]
gethostbynamel() returns an array of all IPv4 addresses, or false on failure. At least this one gives you a proper failure value.
The limitation: Both functions only resolve A records (IPv4). There is no built-in equivalent for AAAA records (IPv6). If you need IPv6 addresses, you must use dns_get_record().
dns_get_record() — The Full-Featured Option
dns_get_record() is PHP's most capable DNS function. It can query any record type and returns structured arrays with all the fields you would expect from a proper DNS response.
$records = dns_get_record('example.com', DNS_A);
The second parameter is a type constant. Here are all the available constants:
DNS_A, DNS_AAAA, DNS_MX, DNS_TXT, DNS_NS, DNS_SOA, DNS_CNAME, DNS_SRV, DNS_CAA, DNS_PTR, DNS_HINFO, DNS_A6, DNS_ALL, DNS_ANY
A and AAAA Records
$a = dns_get_record('example.com', DNS_A);
// [['host' => 'example.com', 'class' => 'IN', 'ttl' => 3600, 'type' => 'A', 'ip' => '93.184.216.34']]
$aaaa = dns_get_record('example.com', DNS_AAAA);
// [['host' => 'example.com', ..., 'type' => 'AAAA', 'ipv6' => '2606:2800:220:1:248:1893:25c8:1946']]
Notice the field naming: A records use ip, AAAA records use ipv6. This inconsistency is something you need to account for when writing generic record-handling code.
MX Records
MX records include a priority value (pri). Lower numbers mean higher priority — the mail server that senders should try first.
$mx = dns_get_record('gmail.com', DNS_MX);
// Sort by priority
usort($mx, fn($a, $b) => $a['pri'] - $b['pri']);
foreach ($mx as $record) {
echo "{$record['pri']} {$record['target']}\n";
}
TXT Records
TXT records are used for SPF, DKIM, DMARC, domain verification, and other metadata. Each record has a txt field with the full string value.
$txt = dns_get_record('example.com', DNS_TXT);
foreach ($txt as $record) {
echo $record['txt'] . "\n";
if (str_starts_with($record['txt'], 'v=spf1')) {
echo " → SPF record found\n";
}
}
For a deep dive into SPF, DKIM, and DMARC records and how they work together for email authentication, see SPF, DKIM, and DMARC Explained.
NS Records
$ns = dns_get_record('example.com', DNS_NS);
foreach ($ns as $record) {
echo $record['target'] . "\n";
}
SOA Records
The SOA (Start of Authority) record contains zone metadata. The fields map directly to the standard SOA record format:
$soa = dns_get_record('example.com', DNS_SOA);
// $soa[0]['mname'] — primary nameserver
// $soa[0]['rname'] — admin email (dot-separated, e.g., admin.example.com)
// $soa[0]['serial'] — zone serial number
// $soa[0]['refresh'] — refresh interval in seconds
// $soa[0]['retry'] — retry interval in seconds
// $soa[0]['expire'] — zone expiry time in seconds
// $soa[0]['minimum-ttl'] — minimum TTL for the zone
Note: the admin email in the rname field uses dots instead of @. So admin.example.com means [email protected].
SRV Records
$srv = dns_get_record('_sip._tcp.example.com', DNS_SRV);
foreach ($srv as $record) {
echo "Priority: {$record['pri']}, Weight: {$record['weight']}, ";
echo "Port: {$record['port']}, Target: {$record['target']}\n";
}
CAA Records
$caa = dns_get_record('example.com', DNS_CAA);
foreach ($caa as $record) {
echo "Flags: {$record['flags']}, Tag: {$record['tag']}, Value: {$record['value']}\n";
}
dns_get_record() Error Handling
This is where PHP's DNS support shows its age. dns_get_record() returns false on complete failure and generates an E_WARNING. It does not throw an exception. And when a domain simply has no records of the requested type, it returns an empty array.
$records = @dns_get_record('nonexistent.example.com', DNS_A);
if ($records === false || empty($records)) {
echo "DNS lookup failed or no records found";
}
The @ operator suppresses the warning, but it also suppresses useful error information. You cannot distinguish between "this domain does not exist" (NXDOMAIN) and "there was a network error" without additional work. This is a real design flaw compared to libraries like Python's dnspython, which raises distinct exceptions for each failure mode.
A slightly better approach is to use a custom error handler:
set_error_handler(function ($errno, $errstr) {
throw new RuntimeException($errstr, $errno);
});
try {
$records = dns_get_record('nonexistent.example.com', DNS_A);
if (empty($records)) {
echo "No records found";
}
} catch (RuntimeException $e) {
echo "DNS error: " . $e->getMessage();
} finally {
restore_error_handler();
}
This converts the warning into an exception, which gives you a proper stack trace and the ability to handle the error in a structured way.
checkdnsrr() — Boolean DNS Check
When you just need a yes/no answer — "does this domain have MX records?" — checkdnsrr() is the right tool. It returns true if at least one record of the specified type exists, false otherwise.
if (checkdnsrr('example.com', 'MX')) {
echo "Domain has MX records";
}
if (checkdnsrr('example.com', 'A')) {
echo "Domain resolves to an IP";
}
The second parameter is a string (not a constant like dns_get_record() uses): 'A', 'MX', 'NS', 'TXT', 'AAAA', 'SOA', 'CNAME', 'SRV', 'CAA', 'PTR', 'ANY'.
This function is perfect for email validation, which I cover in detail below. It is lightweight and fast because it only needs to determine existence, not fetch and parse the full record data.
Reverse DNS: gethostbyaddr()
Reverse DNS maps an IP address back to a hostname. It queries PTR records in the in-addr.arpa (IPv4) or ip6.arpa (IPv6) zones.
$hostname = gethostbyaddr('8.8.8.8');
echo $hostname; // dns.google
// Returns the IP on failure (same gotcha as gethostbyname)
$hostname = gethostbyaddr('192.0.2.1');
if ($hostname === '192.0.2.1') {
echo "No PTR record found";
}
Same design issue as gethostbyname() — failure returns the input string instead of false. Always compare the output with the input to detect failures.
Reverse DNS is commonly used for verifying mail server identities, logging client hostnames, and security auditing. Keep in mind that not every IP address has a PTR record — reverse DNS is optional and configured by the owner of the IP address block.
PHP's Big Limitation: No Custom Nameservers
Every built-in PHP DNS function uses the system resolver — whatever is configured in /etc/resolv.conf on Linux/macOS or the Windows DNS settings. You cannot pass a specific nameserver like 8.8.8.8 or 1.1.1.1 to any of these functions.
This matters more than you might think:
- Checking propagation — You cannot query multiple resolvers to see if a DNS change has reached them. For that, use the Propagation Checker, which queries 30+ resolvers worldwide.
- Querying authoritative servers directly — You cannot bypass caches to check what the authoritative nameserver is actually returning. The DNS Inspector lets you do this from a browser.
- Bypassing cached results — If your system resolver has a stale cached record, there is no way to force a fresh lookup with built-in functions.
Solution: Net_DNS2
Net_DNS2 is a pure PHP DNS library that gives you full control over DNS queries, including custom nameservers, TCP/UDP transport selection, and proper DNSSEC support.
// Install: composer require pear/net_dns2
use Net_DNS2_Resolver;
$resolver = new Net_DNS2_Resolver(['nameservers' => ['8.8.8.8']]);
try {
$result = $resolver->query('example.com', 'A');
foreach ($result->answer as $record) {
echo $record->address . "\n";
}
} catch (Net_DNS2_Exception $e) {
echo "DNS error: " . $e->getMessage();
}
With Net_DNS2, you can query any nameserver, set custom timeouts, use TCP transport, and handle DNS-specific errors properly. It also supports DNSSEC validation — see What Is DNSSEC? for background on why that matters.
Query Authoritative Servers Directly
$resolver = new Net_DNS2_Resolver(['nameservers' => ['8.8.8.8']]);
// Step 1: Find the authoritative nameservers
$ns_result = $resolver->query('example.com', 'NS');
$ns_host = $ns_result->answer[0]->nsdname;
// Step 2: Resolve the nameserver's IP
$a_result = $resolver->query($ns_host, 'A');
$ns_ip = $a_result->answer[0]->address;
// Step 3: Query the authoritative server directly
$auth_resolver = new Net_DNS2_Resolver(['nameservers' => [$ns_ip]]);
$result = $auth_resolver->query('example.com', 'A');
foreach ($result->answer as $record) {
echo "Authoritative answer: {$record->address}\n";
}
If you are building an event-loop application with ReactPHP, also consider ReactPHP/dns for non-blocking async DNS queries:
// Install: composer require react/dns
$loop = React\EventLoop\Loop::get();
$resolver = (new React\Dns\Resolver\Factory())->create('8.8.8.8');
$resolver->resolve('example.com')->then(function (string $ip) {
echo "IP: $ip\n";
});
$loop->run();
Real-World Example: Email Domain Validation
Here is a practical function that validates whether an email address's domain can actually receive mail. It checks MX records, falls back to A records (per RFC 5321), and inspects SPF configuration:
function validateEmailDomain(string $email): array
{
$parts = explode('@', $email);
if (count($parts) !== 2) {
return ['valid' => false, 'reason' => 'Invalid email format'];
}
$domain = $parts[1];
$errors = [];
// Check MX records
$mx = dns_get_record($domain, DNS_MX);
if (empty($mx)) {
// Fall back to A record (some domains accept mail without MX)
$a = dns_get_record($domain, DNS_A);
if (empty($a)) {
return ['valid' => false, 'reason' => 'Domain has no MX or A records'];
}
$errors[] = 'No MX records (using A record fallback)';
}
// Check SPF
$txt = dns_get_record($domain, DNS_TXT);
$hasSpf = false;
foreach ($txt ?? [] as $record) {
if (str_starts_with($record['txt'] ?? '', 'v=spf1')) {
$hasSpf = true;
break;
}
}
if (!$hasSpf) {
$errors[] = 'No SPF record found';
}
return [
'valid' => true,
'domain' => $domain,
'mx_count' => count($mx ?? []),
'has_spf' => $hasSpf,
'warnings' => $errors,
];
}
Usage:
$result = validateEmailDomain('[email protected]');
// ['valid' => true, 'domain' => 'gmail.com', 'mx_count' => 5, 'has_spf' => true, 'warnings' => []]
$result = validateEmailDomain('[email protected]');
// ['valid' => false, 'reason' => 'Domain has no MX or A records']
This approach catches a large class of invalid email addresses that pass simple regex validation — typo domains, expired domains, and domains with no mail infrastructure. It is not a substitute for sending a confirmation email, but it is a fast first-pass filter that saves your mail queue from obvious dead ends.
Performance Considerations
All PHP DNS functions are blocking and synchronous. When you call dns_get_record(), PHP halts execution until the DNS response arrives or the query times out. In a high-traffic web application, slow DNS lookups can pile up and exhaust your PHP-FPM worker pool.
Strategies to mitigate this:
- Cache results — Store DNS responses in APCu, Redis, or Memcached with a TTL matching the record's DNS TTL. This eliminates repeated lookups for the same domain.
- Set OS-level timeouts — Configure your system resolver timeout (in
/etc/resolv.confon Linux:options timeout:2 attempts:1) to fail fast rather than waiting the default 5+ seconds. - Use ReactPHP/dns — For applications that need non-blocking DNS, ReactPHP's async resolver lets you fire off multiple lookups concurrently without blocking.
- Batch where possible — If you need to validate 100 email domains, do not call
dns_get_record()100 times synchronously. Use a queue and process them in a background job, or useNet_DNS2with multiple queries.
// Simple APCu caching wrapper
function cached_dns_lookup(string $domain, int $type = DNS_A, int $ttl = 300): array
{
$key = "dns:{$domain}:{$type}";
$cached = apcu_fetch($key, $success);
if ($success) {
return $cached;
}
$records = dns_get_record($domain, $type) ?: [];
apcu_store($key, $records, $ttl);
return $records;
}
Function Reference
Quick reference for all PHP DNS functions:
| Function | Returns | Record Types | Custom NS |
|---|---|---|---|
gethostbyname() | string | A only | No |
gethostbynamel() | array|false | A only | No |
dns_get_record() | array|false | All | No |
checkdnsrr() | bool | All | No |
gethostbyaddr() | string | PTR | No |
| Net_DNS2 | objects | All | Yes |
The "Custom NS" column is the key differentiator. If your use case requires querying specific nameservers, Net_DNS2 is your only option in PHP.
Under the Hood
To understand what dns_get_record() does under the hood — how DNS packets are structured, how the query is sent to the resolver, and how the response is parsed — see Build a DNS Resolver from Scratch in PHP. That article goes from zero to a working DNS resolver using raw sockets, which gives you a much deeper understanding of what PHP's built-in functions are abstracting away.
Putting It All Together
Between the five built-in functions and Net_DNS2, PHP can handle any DNS task:
| Need | Tool |
|---|---|
| Resolve a hostname to an IPv4 address | gethostbyname() or gethostbynamel() |
| Query MX, TXT, NS, SOA, SRV, CAA, AAAA records | dns_get_record() |
| Check if a record exists (boolean) | checkdnsrr() |
| Reverse DNS (IP to hostname) | gethostbyaddr() |
| Query a specific nameserver | Net_DNS2 |
| Non-blocking async lookups | ReactPHP/dns |
For interactive DNS exploration without writing code, try the DNS Inspector — it lets you query any record type against any nameserver directly from a browser, with the same level of detail you get from dns_get_record() or dig.
For more DNS tools and techniques, see the dig command guide for command-line queries, Build a DNS Resolver in PHP to understand the protocol internals, and Understanding DNS Record Types for a reference on every record type covered in this guide.
