A unique lookup leaves your device, your real resolver fetches it from our server, and we record which resolver asked.
Most people assume the answer is whatever they set in their settings. In practice the path can be redirected by your operating system, your router, your ISP, a VPN, or a public Wi-Fi network, often with no visible sign. The only reliable way to know is to watch where the query actually comes out.
Ask a question only we can hear
The test generates a one-time, unique name for your session: a random label placed in front of a domain we control. Your browser then asks your system to look that name up.
Because the name has never existed before, it cannot be sitting in any cache. To answer it, the lookup has to travel all the way to our server. Whatever resolver makes that final request reveals its own public address to us along the way.
Watch at the source
When that query reaches our server, it is read the instant it arrives. Two things are pulled from it: the address of the resolver that actually asked, and the unique label from the name, so we know the query belongs to your test and not someone else's.
That pairing is held for a few minutes. Your browser then asks us which resolvers showed up for your session, and the page shows you the result. What it reports is which resolver did the lookup, not whether your query reached it encrypted - the checks that answer that are per-protocol and separate.
Why the lookup has to reach us
The test name lives in its own zone, leak.dnsdoh.art, that is delegated to a nameserver we run. Delegation is the ordinary way the DNS hands responsibility for one branch of the tree to a specific server.
leak.dnsdoh.art. NS ns.leak.dnsdoh.art. ns.leak.dnsdoh.art. A 194.180.189.33
Because that branch is delegated to our own nameserver, every name ending in .leak.dnsdoh.art is answered by the machine at 194.180.189.33 and nowhere else. No cache and no third party can stand in for it.
When your resolver meets a name it has never seen, it cannot take a shortcut. It walks the chain from the root down to our nameserver and sends the final question to us directly. The source address on that question is the resolver itself, and that address is what we record.
Two details make the reading robust: the name is compared without regard to letter case, because resolvers deliberately randomise it, and anything arriving from a private or local address is ignored. Your browser polls a small endpoint that returns the addresses seen for your session, and the page groups and names them.
A single test sends several unique names, not just one. Some providers spread their lookups across many machines, so asking more than once lets the whole pool show up instead of a single address.
Running it without a browser
Nothing above needs a browser. The page mints names and polls a small endpoint, and both halves are ordinary commands. That matters on the machines where this question is hardest to answer: a headless server, a router, a container, a box you reached over SSH.
A session is just an identifier you invent. Put it at the front of the label, resolve one name per resolver you want to test, then ask the endpoint what showed up for that session.
$ dig +short @194.180.189.33 "$S-01-aaaa1111-bbb2222.leak.dnsdoh.art" A
$ dig +short @1.1.1.1 "$S-02-cccc3333-ddd4444.leak.dnsdoh.art" A
$ sleep 5
194.180.189.33 194.180.189.33 # the answer is always our nameserver, and is not the point
The wait is not decoration. Recording the query and answering the poll are separate steps, so asking too early returns "total_found": 0 for a test that is working perfectly well. If the first answer is empty, run the poll again rather than starting over.
Both lookups return the same address, because every name in that zone does. The measurement is not in the answer, it is in who came to collect it. That is what the endpoint reports:
{ "ip_addresses": [ "162.159.96.7", "109.200.195.201" ], "details": [ { "ip": "162.159.96.7", "org": "Cloudflare, Inc.", "asn": "AS13335", "city": "Helsinki", "resolver": "Cloudflare" }, { "ip": "109.200.195.201", "org": "i3D.net B.V", "asn": "AS49544", "city": "Spånga", "resolver": "Quad9" } ], "total_found": 2, "client_ip": "203.0.113.10" }
That is a real result from two lookups made seconds apart, and the interesting part is that neither address is the resolver that was asked. The second lookup went to 1.1.1.1 and appears as a Cloudflare machine in Helsinki. The first went to this service, which forwards upstream, so what arrived at the authoritative was a Quad9 egress in Sweden - not 194.180.189.33, which never appears at all.
This is the single most misread thing about any leak test. What it shows is the resolver that reached the authoritative server. Where your resolver forwards to somebody else, that somebody is what you see, and the address you configured is invisible. A result full of names you do not recognise is normal for a forwarding setup; it is only a leak if the network behind them is one you did not choose - your ISP being the classic case.
Two more things the terminal makes obvious. Repeating the poll a few seconds later often returns more addresses, because large providers spread one lookup across a pool. And on a machine whose DNS is handled by something local - a VPN client, a tailnet, a container runtime - that local address can appear in the list too, which tells you where the query went first.
This is not a public API and there is no contract behind it: it exists to serve the page on this site, it is rate-limited, and its shape can change without notice. Use it to answer a question about a machine in front of you, not as something to build on.
The endpoint is rate-limited at the connection level - a burst of fifteen calls from one address got fourteen dropped and one answered - so poll it a few times, not in a loop. If it stops responding entirely, wait a minute rather than retrying harder.
One failure worth recognising: {"error":"Missing session ID"} means $S was empty, which is what happens if the poll runs in a different shell from the one that minted it. Run the four commands in the same session, or paste the identifier in by hand.
The endpoint needs no key, takes any session identifier you invent, and forgets what it saw within minutes. It reports resolvers, not encryption: proving the query travelled encrypted is a separate check, one per protocol, set out in whether your DNS was really encrypted.
Why this approach is honest
Reality, not settings
It sees the resolver that did the work, not the one your configuration claims.
Catches interception
Some networks quietly redirect DNS and answer it themselves. A forced fresh lookup makes that show up as an unexpected address.
Per-session, temporary
Each test uses its own throwaway name, and the captured addresses are kept only briefly before they expire.
What it does not do
It records the addresses of the resolvers that queried the test domain, for a few minutes, to show you the result. It does not log your browsing history, and a resolver's address is not your home address unless your device resolves DNS itself.
The technique also has a failure mode worth knowing about, and ours hit it: if the test's own authoritative nameserver does not answer, your device falls back to a second resolver and the test records that fallback as though it were a leak. We found and fixed exactly that on 28 July 2026 - the full account is in when our leak test invented a leak.
See your own result
The result is a direct measurement: the servers that answered for you, captured at the moment they asked.
Run the DNS leak test