reactphp/dns: an asynchronous DNS resolver that also speaks DNS
Async DNS resolver for ReactPHP.
At a glance
- What is it?
- The ReactPHP DNS component is an async DNS resolver built on promises, but the readme is candid that it is really a toolkit for working with DNS messages and could easily be used to create a DNS server. It caches, honours the hosts file, and offers pluggable transports for UDP, TCP and a selective executor that upgrades between them.
- Who is it for?
- Adopt reactphp/dns if you are running PHP event loop code that needs to resolve names without blocking, since that is what it is built for and the promise API fits ReactPHP naturally. Do not adopt it as a general purpose DNS library for batch lookups, because the blocking system config load is a real cost if you call it in a loop.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 17 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
An async resolver that is really a DNS toolkit
The ReactPHP DNS component is an async DNS resolver for ReactPHP, and the readme makes an important qualification about what that means in practice.
The main point of the DNS component is to provide async DNS resolution. However, it is really a toolkit for working with DNS messages, and could easily be used to create a DNS server.
That single sentence tells you how to evaluate the package. If you arrive expecting a thin async wrapper around the system resolver, you will find something more capable. The message parsing and construction is exposed, the transports are pluggable, and the readme is telling you that the same machinery can serve as a server rather than only a client.
The problem it solves is the classic one for a PHP program. The standard resolver is blocking, and in an event loop that means one slow DNS lookup stops every other connection the process is handling. The fix here is the promise model, which is ReactPHP's whole idea: you get a promise back and continue.
The basic usage is short. You build a config, create a resolver through the factory, and resolve:
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->create($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});There is a detail in that snippet worth pausing on, because it is the kind of thing that bites in production. The Config class can be used to load the system default config, and the readme says this is an operation that may access the filesystem and block, and that ideally this method should thus be executed only once before the loop starts and not repeatedly while it is running.
The same warning appears again about the hosts file. The factory loads the hosts file from the filesystem once when creating the resolver instance, and the same advice applies: do it before the loop starts.
So the resolver itself is non-blocking, but the setup that creates it is not, and the readme is careful to say so in two places. The fallback to 8.8.8.8 in the example is there because the config may come back empty, and the readme explains that the class may return an empty configuration if the system config can not be loaded, so you will likely want to apply a default nameserver as shown.
Caching and the promise model
Caching is the second thing you will want, and it is configured by using a different factory method.
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
...
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});The readme explains the behaviour precisely: if the first call returns before the second, only one query will be executed, and the second result will be served from an in memory cache. The example makes the timing explicit with the ellipsis between the two calls.
The readme says this is particularly useful for long running scripts where the same hostnames have to be looked up multiple times, and that is the case that matters. A one-shot lookup gains nothing from a cache, but a long-lived worker that resolves the same set of hosts on every request does, and the saving is not just the round trip, it is the load on the nameserver.
The default cache is in memory. You can substitute your own by implementing the CacheInterface from the ReactPHP cache package, and the factory accepts it as an argument:
$cache = new React\Cache\ArrayCache();
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached('8.8.8.8', null, $cache);Note that this call passes the nameserver as a plain string rather than a config object, with null in the middle position. So the factory takes either form, which is convenient when you want to skip the system config entirely, as this example does, and the cache is the third argument.
The readme points at the ReactPHP wiki for possible cache implementations, which is the usual route into that ecosystem's shared components.
resolve and resolveAll, and the errors they distinguish
The ResolverInterface has two main methods, and the difference between them is about how much you get back and how much control you have.
The resolve method resolves the given domain name to a single IPv4 address with a type A query:
$resolver->resolve('reactphp.org')->then(function ($ip) {
echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
});The readme calls this one of the main methods in the package: it sends a DNS query for the given domain name to your DNS server and returns a single IP address on success.
The important behaviour is what happens when there is more than one answer. If the DNS server sends a DNS response message that contains more than one IP address for this query, it will randomly pick one of the IP addresses from the response. If you want the full list of IP addresses or want to send a different type of query, you should use the resolveAll method instead.
So resolve is the convenience wrapper and resolveAll is the general one. The random pick is a deliberate choice, since load balancing across addresses is a reasonable default, but it means resolve is not the right call if you need to connect to a specific address.
The resolveAll method takes a host and a query type and returns a list with all record values:
$resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});The Message::TYPE constants control the query type, and the readme is explicit about what the return value contains depending on the type. It always returns a list of record values, but each record value type depends on the query type: IPv4 addresses for A queries, IPv6 addresses for AAAA queries, the hostname for NS, CNAME and PTR queries, and structured data for other queries.
That last clause is the reason resolveAll is the toolkit entry point rather than just a convenience. With the message constants you can query any record type and get structured data back, not just addresses.
The error handling is described consistently across both methods, and the distinction between the two failure modes is worth knowing. If the DNS server sends a DNS response message that indicates an error code, the promise rejects with a RecordNotFoundException, and its message and code can be used to check for the response code. If the DNS communication fails and the server does not respond with a valid response message, the promise rejects with an Exception.
So a server that answered and said no is different from a server that never answered, and the exception types tell them apart.
There is one more capability in the interface worth noting. Pending DNS queries can be cancelled by cancelling the pending promise:
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();For a timeout in an event loop that is the natural mechanism, since the promise is the handle on the in-flight query.
The executor layer: UDP, TCP, selective and the hosts file
The advanced usage section is where the toolkit nature of the package shows, because the transport is a separate, swappable layer.
The table of contents lists four things under advanced usage. UdpTransportExecutor, TcpTransportExecutor, SelectiveTransportExecutor and HostsFileExecutor.
The two transports are the two ways DNS is carried on the wire. UDP is the normal case and it is fast because there is no connection setup. TCP is the fallback for cases where UDP is not enough, which in practice means large responses that would not fit in a UDP datagram and the truncation bit that signals it, and also the case where the resolver is expected to work over a network that blocks UDP.
Having them as separate executors rather than one implementation with a flag means you can decide per instance, and it means the package does not have to guess for you.
SelectiveTransportExecutor is the interesting one, and the name says what it does. It selects between the transports depending on the query, so a resolver can use UDP for ordinary lookups and fall back to TCP when a response comes back truncated. That is the behaviour a stub resolver has to have to be correct, and doing it in the executor means the higher level code does not have to know about it.
HostsFileExecutor is the fourth, and it is what makes the package behave like the rest of the system. The readme notes that the factory loads the hosts file from the filesystem once when creating the resolver instance, which means the resolver respects the local hosts file. That is a requirement rather than a nicety in many environments, since a container with an entry in its hosts file expects name resolution to honour it.
The existence of these four executors as separate documented pieces is the clearest evidence for the readme's claim that this could easily be used to create a DNS server. A server needs to send messages rather than receive answers, and a toolkit where the message layer, the transport layer and the record layer are all separable is exactly what you would build one from.
The examples directory reinforces this. Alongside the first example there is 02-concurrent.php and 03-cached.php, then a set with two-digit prefixes that look like a second series: 11-all-ips.php, 12-all-types.php and 13-reverse-dns.php, and then 91-query-a-and-aaaa.php and 92-query-any.php. The naming suggests a progression from simple to advanced, with the 90s reserved for the query-shape examples.
Two version lines, and how the project is laid out
The readme opens with a development version notice, and it is the kind of notice that tells you how the project is run.
This branch contains the code for the upcoming v3 release, and for the code of the current stable v1 release you check out the 1.x branch. The readme then says the upcoming v3 release will be the way forward for this package, but that v1 will still be actively supported for those not yet on the latest version.
So there are two supported lines at once, which is a deliberate choice for a library that other people depend on. The repository default branch here is 3.x, and the recent releases are v1.14.0 from 2025-11-18, v1.13.0 from 2024-06-13 and v1.12.0 from 2023-11-29, so the stable line is still receiving patches while the major version is developed on a separate branch. The readme points at the installation instructions for more details on which to use.
The top level of the repository is the usual Composer layout, with src/ for the code, tests/ and two PHPUnit configuration files, phpunit.xml.dist and phpunit.xml.legacy, the second being there for the older release line. There is a composer.json, a CHANGELOG.md, an examples/ directory and the usual GitHub and metadata directories.
The split of the source into a resolver layer, a message layer and a transport layer is visible in the API surface described in the readme, and the examples directory follows the same progression. The first example is the basic resolve, the second is concurrent, the third is cached, and then the numbered series that covers getting all addresses, all record types, reverse DNS, and finally the two query-shape examples for querying A and AAAA together and for querying ANY.
The reverse DNS example is worth noting for what it implies. A PTR query is a different direction of travel from the others, and the readme already says resolveAll returns the hostname for PTR queries, so the machinery handles the reverse direction as well as the forward one. That is another sign of the toolkit framing rather than the wrapper framing.
The package is MIT licensed and the homepage is reactphp.org/dns, and the last push was on 2026-10-06, so the 3.x branch is being worked on while the 1.x line stays in maintenance.
Editorial conclusion
Adopt reactphp/dns if you are running PHP event loop code that needs to resolve names without blocking, since that is what it is built for and the promise API fits ReactPHP naturally. Do not adopt it as a general purpose DNS library for batch lookups, because the blocking system config load is a real cost if you call it in a loop. Before you commit, load the system config once before the loop starts rather than per request, and pin the version you need, since the 3.x branch is a development version while v1 remains the stable line for now.
Frequently asked questions
What does the ReactPHP DNS package do?
It provides async DNS resolution for ReactPHP. The readme is candid that the main point is async resolution but that it is really a toolkit for working with DNS messages and could easily be used to create a DNS server. It is MIT licensed and its homepage is reactphp.org/dns.
How do I resolve a hostname with reactphp/dns?
Load the config with React\Dns\Config\Config::loadSystemConfigBlocking(), apply a default nameserver if none was found, then create a resolver with the factory and call resolve, which returns a promise for a single IPv4 address. If the response contains more than one address, resolve randomly picks one of them.
What is the difference between resolve and resolveAll?
resolve returns a single IPv4 address from a type A query and randomly picks one if the response has several. resolveAll takes a host and a query type and returns a list of all record values, using the Message::TYPE constants to choose the type, so it returns IPv6 addresses for AAAA, hostnames for NS, CNAME and PTR, and structured data for other types.
How do I enable caching in reactphp/dns?
Use createCached on the factory instead of create. The default is an in memory cache, so if the first call returns before the second, only one query is executed and the second result is served from the cache. You can supply your own cache implementing the CacheInterface from the ReactPHP cache package as the third argument.
Which transports does reactphp/dns support?
The advanced usage section lists UdpTransportExecutor, TcpTransportExecutor, SelectiveTransportExecutor and HostsFileExecutor. The selective executor picks between UDP and TCP depending on the query, and the hosts file executor makes the resolver honour the local hosts file, which the factory loads once when the resolver instance is created.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/reactphp-dns)