
How to Perform a Reverse DNS Lookup?
An unfamiliar IP address shows up in your server logs a few thousand times an hour. A mail server rejects your messages with a vague note about reverse DNS. A firewall report lists 300 source addresses and nobody knows who they belong to. In each case, the first thing you want to do is turn the IP addresses back into names, which is exactly what a reverse DNS lookup does.
This is a hands-on guide. It shows how to run reverse lookups with every common command-line tool and in several programming languages, how to look up hundreds or thousands of addresses efficiently, and, most importantly, how to interpret what comes back, including empty results, generic ISP hostnames, and mismatches. If you want the background on the PTR records that power all of this, read what PTR records are and when they're used first; this article won't repeat it.
The One-Line Answer
On Linux or macOS:
dig -x 203.0.113.25 +short
On Windows:
Resolve-DnsName 203.0.113.25
Both return the hostname associated with the IP address, if there is one. The rest of this article covers the alternatives, scaling up, and making sense of the output.
Reverse Lookups with Command-Line Tools
dig
# IPv4
dig -x 203.0.113.25 +short
# IPv6
dig -x 2001:db8::25 +short
# Full answer with TTL
dig -x 203.0.113.25 +noall +answer
-x converts the address into the special reverse name (25.113.0.203.in-addr.arpa for IPv4, a long nibble-reversed name under ip6.arpa for IPv6) and queries it for PTR records. +noall +answer shows the TTL and the exact owner name, which helps when debugging delegation. The full dig guide covers its other options.
To ask a specific resolver, add @:
dig @1.1.1.1 -x 203.0.113.25 +short
host
host 203.0.113.25
25.113.0.203.in-addr.arpa domain name pointer mail.example.com.
host detects that the argument is an IP address and performs a reverse lookup automatically. Its one-line output is easy to read and easy to parse.
nslookup
nslookup 203.0.113.25
25.113.0.203.in-addr.arpa name = mail.example.com.
Like host, nslookup recognizes an IP address and queries the PTR record. On Windows the output instead shows Name: and Address: lines. You can also be explicit with nslookup -type=PTR 25.113.0.203.in-addr.arpa. See the nslookup guide for interpreting its error messages.
PowerShell
Resolve-DnsName -Name 203.0.113.25 -Type PTR
# Just the hostname
(Resolve-DnsName -Name 203.0.113.25 -Type PTR -ErrorAction SilentlyContinue).NameHost
Resolve-DnsName returns an object whose NameHost property holds the PTR target. -ErrorAction SilentlyContinue makes it return nothing instead of throwing an error when no PTR record exists, which is convenient in scripts.
getent (Linux)
getent hosts 203.0.113.25
getent goes through the system resolver, so it also checks /etc/hosts. Use it when you want to know what applications on the machine will see, not just what DNS says.
Reverse Lookups in Code
Python (standard library)
import socket
def reverse_lookup(ip: str) -> str | None:
try:
hostname, _aliases, _addrs = socket.gethostbyaddr(ip)
return hostname
except (socket.herror, socket.gaierror):
return None
print(reverse_lookup("203.0.113.25"))
socket.gethostbyaddr() uses the operating system resolver and raises socket.herror when there's no PTR record. It has no timeout parameter of its own, so a slow resolver can block it for several seconds.
Python with dnspython
For control over timeouts, resolvers, and TTLs, use dnspython (pip install dnspython):
import dns.resolver
import dns.reversename
resolver = dns.resolver.Resolver()
resolver.nameservers = ["1.1.1.1"]
resolver.lifetime = 3.0
def reverse_lookup(ip: str) -> list[str]:
name = dns.reversename.from_address(ip)
try:
answer = resolver.resolve(name, "PTR")
return [str(r.target).rstrip(".") for r in answer]
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
return []
print(dns.reversename.from_address("203.0.113.25")) # 25.113.0.203.in-addr.arpa.
print(reverse_lookup("203.0.113.25"))
dns.reversename.from_address() builds the reverse name for both IPv4 and IPv6. The function returns a list because, while rare, an address can have multiple PTR records. dnspython also offers dns.resolver.resolve_address(ip) as a shortcut for the same thing.
Node.js
import { reverse } from "node:dns/promises";
async function reverseLookup(ip) {
try {
return await reverse(ip);
} catch (err) {
if (err.code === "ENOTFOUND" || err.code === "ENODATA") return [];
throw err;
}
}
console.log(await reverseLookup("203.0.113.25"));
dns/promises reverse() sends a real DNS PTR query and returns an array of hostnames. Run it as an ES module (save it as reverse.mjs, or set "type": "module" in package.json) so top-level await works.
Go
package main
import (
"fmt"
"net"
)
func main() {
names, err := net.LookupAddr("203.0.113.25")
if err != nil {
fmt.Println("no PTR record:", err)
return
}
fmt.Println(names)
}
net.LookupAddr returns a slice of names, each with a trailing dot, or an error when no PTR record exists.
Bulk Reverse Lookups
A simple shell loop
Given a file ips.txt with one address per line:
while read -r ip; do
name=$(dig -x "$ip" +short | head -n 1)
printf '%s\t%s\n' "$ip" "${name:-NO_PTR}"
done < ips.txt
This prints a tab-separated list of each IP and its first PTR name, or NO_PTR if there isn't one. It's sequential, so a few hundred addresses can take a while.
Parallel lookups with xargs
xargs -P 10 -I{} sh -c 'printf "%s\t%s\n" "{}" "$(dig -x {} +short +time=2 +tries=1 | head -n 1)"' < ips.txt
-P 10 runs ten lookups at a time, and +time=2 +tries=1 stops any single unresponsive reverse zone from stalling the run. Output order won't match input order, so sort afterward if needed.
Extracting unique IPs from a web server log
awk '{print $1}' /var/log/nginx/access.log | sort -u > ips.txt
This pulls the client address (the first field in the default combined log format) and deduplicates it, giving you a clean input file. Resolve names offline like this rather than enabling hostname lookups in the web server itself, which slows every request.
Concurrent lookups in Python
import csv
import sys
from concurrent.futures import ThreadPoolExecutor
import dns.resolver
import dns.reversename
resolver = dns.resolver.Resolver()
resolver.lifetime = 2.0
def ptr(ip: str) -> tuple[str, str]:
try:
answer = resolver.resolve(dns.reversename.from_address(ip), "PTR")
return ip, str(answer[0].target).rstrip(".")
except dns.resolver.NXDOMAIN:
return ip, "NXDOMAIN"
except dns.resolver.NoAnswer:
return ip, "NO_PTR"
except (dns.resolver.NoNameservers, dns.resolver.LifetimeTimeout):
return ip, "LOOKUP_FAILED"
with open("ips.txt") as f:
ips = [line.strip() for line in f if line.strip()]
writer = csv.writer(sys.stdout)
writer.writerow(["ip", "ptr"])
with ThreadPoolExecutor(max_workers=20) as pool:
for row in pool.map(ptr, ips):
writer.writerow(row)
This resolves the list with 20 worker threads and writes a CSV. Distinguishing NXDOMAIN, NO_PTR, and LOOKUP_FAILED matters: the first two mean "no name," while the third means "couldn't find out," and they call for very different follow-up.
Sweeping a whole subnet
To see every PTR record in a /24:
for i in $(seq 1 254); do
name=$(dig -x "203.0.113.$i" +short +time=1 +tries=1)
[ -n "$name" ] && echo "203.0.113.$i $name"
done
This prints only addresses that have a PTR record. nmap can do the same with its list scan, which performs reverse DNS on every address without sending any packets to the hosts themselves:
nmap -sL 203.0.113.0/24 | grep '('
Only sweep ranges you own or are authorized to assess; large-scale reverse DNS enumeration of other networks can be seen as reconnaissance.
Interpreting the Results
Getting an answer is easy. Knowing what it means takes a bit more care.
1. A descriptive hostname
mail.example.com or web-03.prod.example.com usually means the IP owner set the PTR record deliberately. It's a strong hint about the organization and the role of the host, but not proof; anyone controlling the reverse zone can set any name.
2. A generic ISP or cloud hostname
Names like 203-0-113-25.dynamic.isp.example.net or ec2-203-0-113-25.compute-1.amazonaws.com mean the address belongs to that provider and nobody customized the PTR. For log analysis, this tells you the network but not the customer. For a mail server you operate, a generic PTR like this is a deliverability problem you should fix with your provider.
3. NXDOMAIN or no answer
Host 25.113.0.203.in-addr.arpa not found: 3(NXDOMAIN)
No PTR record exists for this address. It's very common for client addresses and many servers. It doesn't mean the IP is unused or malicious.
4. SERVFAIL or a timeout
The reverse zone exists but its nameservers are broken, unreachable, or misconfigured. Check who's responsible for the zone:
dig 113.0.203.in-addr.arpa SOA +noall +answer +authority
dig -x 203.0.113.25 +trace
The SOA reveals the reverse zone's operator, and the trace shows where delegation from in-addr.arpa breaks. Response codes are explained in what NXDOMAIN, SERVFAIL, and REFUSED mean.
5. A CNAME in the answer
25.113.0.203.in-addr.arpa. 3600 IN CNAME 25.0-63.113.0.203.in-addr.arpa.
25.0-63.113.0.203.in-addr.arpa. 3600 IN PTR mail.example.com.
This is classless reverse delegation (RFC 2317). The ISP owns the whole /24 but has delegated a smaller block, here .0 to .63, to a customer using CNAMEs. It's normal and resolvers follow it automatically. If the lookup fails after the CNAME, the customer's delegated zone is the broken part.
6. A name that doesn't resolve back
Always verify the reverse result with a forward lookup before trusting it:
ip=203.0.113.25
name=$(dig -x "$ip" +short | head -n 1)
fwd=$(dig "$name" A +short)
echo "$ip -> $name -> $fwd"
echo "$fwd" | grep -qx "$ip" && echo "FCrDNS: pass" || echo "FCrDNS: fail"
If the hostname resolves back to the original IP, the result passes forward-confirmed reverse DNS. If not, treat the name as unverified: the reverse zone's owner can publish any name they like, including one belonging to a domain they don't control. The PTR article explains why mail servers rely on this check.
Online Tools
When you can't run commands, web-based reverse lookup tools such as MXToolbox's reverse lookup do the same query from their own resolvers. They're useful for a second opinion from outside your network, but for bulk work or anything sensitive, use the command-line or code approaches above.
Reverse DNS Lookup FAQ
On Linux and macOS, dig -x <ip> +short or host <ip>. On Windows, nslookup <ip> or PowerShell's Resolve-DnsName <ip>. All of them query the PTR record for the address.
The IP address has no PTR record. Reverse records are set by whoever controls the IP block, usually your ISP, hosting company, or cloud provider, so you need to set or request one there.
Not on its own. The reverse zone owner can publish any name. Confirm it with a forward lookup of that hostname; if it resolves back to the same IP, the result passes forward-confirmed reverse DNS.
The same tools work. dig -x 2001:db8::25 builds the nibble-reversed ip6.arpa name automatically, and Python, Node.js, and Go functions accept IPv6 addresses directly.
Run lookups in parallel with xargs -P or a thread pool in Python, set short timeouts so slow zones don't stall the run, and deduplicate the input list first.
It usually indicates classless reverse delegation under RFC 2317, where an ISP delegates part of a /24 to a customer. Resolvers follow the CNAME to the delegated zone automatically.
Sometimes, through a descriptive hostname, but often only the provider is visible. For authoritative ownership information, look the address up in the regional internet registry's RDAP or WHOIS service.
Reverse zones for many address ranges are poorly maintained, so lookups can wait on unresponsive nameservers until they time out. Set explicit timeouts in your tools and code to keep bulk jobs moving.
Conclusion
Running a reverse DNS lookup is easy with any tool: dig -x, host, nslookup, Resolve-DnsName, or a few lines of Python, Node.js, or Go. The real skill is in scaling up and reading the results correctly. Use parallel lookups with tight timeouts for large lists, keep "no PTR record" distinct from "lookup failed," and recognize generic provider names and RFC 2317 CNAMEs for what they are.
Most of all, never take a reverse result at face value. A PTR record is a claim made by whoever controls the IP block, so confirm it with a forward lookup before you use it to make decisions about logs, abuse reports, or mail servers.
These references cover the standards and tools used above:
- RFC 1035: Domain Names - Implementation and Specification — defines PTR records and the
in-addr.arpadomain. - RFC 3596: DNS Extensions to Support IP Version 6 — defines
ip6.arpafor IPv6 reverse lookups. - RFC 2317: Classless IN-ADDR.ARPA delegation — explains the CNAME-based delegation of reverse zones smaller than a /24.
- dnspython: dnspython documentation — reference for
dns.resolveranddns.reversename. - Node.js: DNS module documentation — covers
dns.promises.reverse()and its error codes.


