PHP's dns_get_record() queries DNS through the system resolver and returns an array of records, each with host, class, ttl, type and type-specific keys such as ip, target or txt. A name that doesn't exist yields an empty array; a resolver failure yields false plus a warning. You can't choose the nameserver or set a timeout.
dns_get_record() looks like one of PHP’s simpler functions. In code review it’s one of the ones I ask about most, and it’s nearly always the same three questions: what comes back for a name that doesn’t exist, what comes back when the resolver fails, and why DNS_ANY is empty.
Calling PHP dns_get_record and reading the result
The signature takes a hostname, a record type bitmask and two optional by-reference arrays for the authority and additional sections.
<?php
declare(strict_types=1);
print_r(dns_get_record('denic.de', DNS_SOA));
Real output:
Array
(
[0] => Array
(
[host] => denic.de
[class] => IN
[ttl] => 4502
[type] => SOA
[mname] => ns1.denic.de
[rname] => dns-operations.denic.de
[serial] => 1791902669
[refresh] => 10800
[retry] => 1800
[expire] => 3600000
[minimum-ttl] => 1800
)
)
Every record has host, class, ttl and type. The rest depends on the type, which is where most Undefined array key warnings come from.
Record types and the keys they return
| Constant | Type | Extra keys |
|---|---|---|
DNS_A | A | ip |
DNS_AAAA | AAAA | ipv6 |
DNS_MX | MX | pri, target |
DNS_NS | NS | target |
DNS_CNAME | CNAME | target |
DNS_TXT | TXT | txt, entries |
DNS_SOA | SOA | mname, rname, serial, refresh, retry, expire, minimum-ttl |
DNS_SRV | SRV | pri, weight, port, target |
DNS_CAA | CAA | flags, tag, value |
DNS_PTR | PTR | target |
TXT deserves a closer look. A TXT record can hold several strings of up to 255 bytes each, which long SPF and DKIM records routinely need. entries has the parts, txt the joined value. For DKIM keys, implode('', $r['entries']) is the safe choice.
Policy records often live on a prefixed name. DMARC, for example:
<?php
declare(strict_types=1);
$dmarc = dns_get_record('_dmarc.php.net', DNS_TXT);
echo $dmarc[0]['txt'] ?? 'no DMARC record', "\n";
// v=DMARC1; p=none; rua=mailto:[email protected];
Types PHP doesn’t know can’t be queried. On PHP 8.5 there’s no constant for HTTPS or SVCB records; defined('DNS_HTTPS') returns false.
Empty array versus false
This is the part that matters most. There are two flavours of “nothing”, and they mean very different things:
| Situation | Return value | Meaning |
|---|---|---|
| Name doesn’t exist (NXDOMAIN) | [] | definite: no records |
| Name exists, type doesn’t | [] | definite: none of that type |
| Resolver failure, timeout, SERVFAIL | false plus warning | unknown |
On PHP 8.5 the warning reads like this, triggered here by DNS_ALL on php.net:
PHP Warning: dns_get_record(): A temporary server error occurred. in Command line code on line 1
Pass that false straight into array_map() and you get a TypeError. A small wrapper makes the distinction explicit:
<?php
declare(strict_types=1);
/** @return list<array<string, mixed>> */
function lookup(string $host, int $type): array
{
$records = @dns_get_record($host, $type);
if ($records === false) {
$error = error_get_last()['message'] ?? 'unknown DNS error';
throw new RuntimeException("DNS lookup for $host failed: $error");
}
return $records;
}
The @ keeps the warning out of your logs while error_get_last() still captures the message. If your framework already converts warnings to exceptions, you can drop both.
Why DNS_ANY and DNS_ALL disappoint
DNS_ANY sends a real ANY query. RFC 8482 (2019) lets servers answer those with a minimal response, and big operators do. In my test, dns_get_record('cloudflare.com', DNS_ANY) returned an empty array, while php.net returned NS, A, AAAA, MX and TXT. You can’t rely on either behaviour.
DNS_ALL queries each type separately but fails completely if any single type errors, which is exactly what happened with php.net. Ask for the types you need: DNS_A | DNS_AAAA | DNS_MX.
Reverse lookups
PTR records work too. Reverse the IPv4 octets and append in-addr.arpa:
<?php
declare(strict_types=1);
$ip = '185.85.0.29';
$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
print_r(dns_get_record($ptr, DNS_PTR));
// [target] => ip-185-85-0-29.ax5z.com
gethostbyaddr() gives the same name with less code. Use dns_get_record() when you need the TTL or expect more than one PTR.
The by-reference $authns and $addtl arrays are less useful than they look. Recursive resolvers often strip those sections, and in my test both stayed empty for an MX query on php.net. Query NS and A records explicitly instead.
When checkdnsrr() is the better call
If you only need a yes or no, checkdnsrr($host, 'MX') is shorter and returns a plain boolean. Note the default type: called without a second argument, it checks MX, not A, which has surprised more than one developer testing whether a hostname resolves. It also folds “doesn’t exist” and “resolver failed” into the same false, so it suits pre-filters and quick sanity checks, not decisions you can’t undo. A typical use is ruling out names that are clearly registered before calling a rate-limited availability API.
What dns_get_record() can’t do
It always uses the system resolver (on Linux, whatever /etc/resolv.conf says). That has consequences:
- You see cached data. The TTL counts down, and after a change the old value can come back for hours.
- You can’t pick the server. For authoritative answers use
pear/net_dns2, which exists on Packagist, ordig @ns1.example.com. - There’s no timeout parameter. The resolver decides. A CAA query against one slow server kept my test script waiting for over a minute.
That last point is a reason to keep DNS lookups out of synchronous web requests where you can. A cron job is a better home, as shown in DNS monitoring with PHP. For mail-specific checks, including null MX and the implicit A-record fallback, see checking MX records in PHP.
Internationalised names need converting first. dns_get_record('bücher.com', DNS_A) queries a name that doesn’t exist in DNS; convert it with idn_to_ascii() as covered in the punycode guide.
If the records you’re checking belong to a domain you’re setting up yourself, the fix is usually at the registrar, not in your code. At betadomain you manage nameservers and DNS in the customer portal; browse the list of all extensions, check what a .com costs per year (€14.99), or look up a name before you build anything around it.
Found your domain? You can check and register it directly at domains at a fixed yearly price – yearly price from €2.99.
FAQ
Why does dns_get_record() with DNS_ANY return an empty array?
Since RFC 8482, nameservers may answer ANY queries minimally, and large providers do. In testing, cloudflare.com returned nothing at all. Query the record types you need explicitly, combined with the | operator.
Can I make dns_get_record() use a specific nameserver?
No. It always goes through the operating system's resolver. To query an authoritative server or a public resolver directly, use a library such as pear/net_dns2 or shell out to dig.
What's the difference between checkdnsrr() and dns_get_record()?
checkdnsrr() returns true if at least one record of the given type exists, nothing more. dns_get_record() returns the records themselves. Use the former for yes/no checks and the latter whenever you need values.
Why does the TTL change on every call?
The local resolver caches answers and reports the remaining lifetime, so the TTL counts down between calls. Only a query to the authoritative server shows the configured value.