nslookup vs curl — DNS Resolution & Debugging
What they do
nslookup — DNS resolver only. Sends a query to a DNS server asking "what IP is this hostname?" Never connects to the target host.
curl — HTTP/HTTPS client. Makes a full application-layer request — fetches content, sends data, tests APIs. DNS resolution is just the first step.
Does nslookup ever actually connect to the target host (e.g. api.example.com) the way curl does?
Are both TCP-based?
No.
| Tool | Protocol | Transport |
|---|---|---|
nslookup |
DNS | UDP port 53 (default), falls back to TCP port 53 for large responses |
curl (HTTP) |
HTTP/1.1, HTTP/2 | TCP port 80 |
curl (HTTPS) |
HTTP over TLS | TCP port 443 |
curl (HTTP/3) |
HTTP/3 | UDP (QUIC) port 443 |
By default, what transport does nslookup use — and what makes it switch?
Full flow comparison
sequenceDiagram
box rgb(52,73,94) Local machine
participant You
end
box rgb(41,84,127) DNS infrastructure
participant DNS as DNS Server (8.8.8.8)
end
box rgb(120,66,18) Remote service
participant Server as api.example.com
end
rect rgb(30,60,90)
Note over You,DNS: nslookup api.example.com — DNS only, nothing else happens
You->>DNS: UDP 53 — Query: A record for api.example.com?
DNS-->>You: UDP 53 — Answer: 93.184.216.34
end
rect rgb(70,45,20)
Note over You,Server: curl https://api.example.com — same DNS step, then a full HTTP request
You->>DNS: UDP 53 — Query: A record for api.example.com?
DNS-->>You: UDP 53 — Answer: 93.184.216.34
You->>Server: TCP SYN → port 443
Server-->>You: TCP SYN-ACK
You->>Server: TLS ClientHello
Server-->>You: TLS ServerHello + Certificate
You->>Server: HTTP GET /
Server-->>You: HTTP 200 OK + body
end
The blue-shaded block is the entirety of what nslookup ever does. The amber-shaded block is everything curl does in addition to that same DNS exchange — DNS resolution is a strict subset of curl's total work, not a separate concern from it.
curl's flow starts with the exact same two messages as nslookup's entire flow. How much of curl's total work do those two messages represent?
How each resolves a domain
curl uses the OS resolver (getaddrinfo) — it goes through the full chain the operating system provides to every application on the box.
graph TD
classDef used fill:#27ae60,stroke:#1e8449,color:#fff
A["curl api.example.com"] --> B{"/etc/hosts has a match?"}
subgraph CHAIN["OS resolver chain - getaddrinfo()"]
B -->|"no match"| D["Read /etc/resolv.conf nameserver"]
D --> E["systemd-resolved stub resolver - 127.0.0.53"]
E --> F["Upstream DNS query - UDP port 53"]
end
B -->|"match found"| C["Use that IP directly - no DNS query sent"]:::used
F --> G["IP address returned to curl"]:::used
Respects /etc/hosts, search domains, ndots, and the local stub resolver cache.
nslookup bypasses the OS resolver entirely — it never calls getaddrinfo() and always sends a raw DNS wire query itself.
graph TD
classDef used fill:#27ae60,stroke:#1e8449,color:#fff
classDef bypassed fill:#7f8c8d,stroke:#616a6b,color:#fff
N["nslookup api.example.com"] --> H["Reads /etc/resolv.conf nameserver directly"]
H --> I["Sends a raw DNS query - UDP port 53 - straight to upstream"]
I --> J["IP address returned"]:::used
K["/etc/hosts"]:::bypassed -.->|"IGNORED"| N
L["systemd-resolved cache - 127.0.0.53"]:::bypassed -.->|"BYPASSED"| N
This is exactly why nslookup and curl can legitimately disagree about whether a hostname resolves — they are not, in general, reading from the same source of truth.
A hostname has a matching entry in /etc/hosts. Which tool will actually use it — curl or nslookup?
The resolver libraries
| Tool | Library | Behavior |
|---|---|---|
nslookup / dig |
Own DNS implementation | Raw DNS queries, no OS resolver chain |
curl (default) |
glibc getaddrinfo() |
Full OS resolver chain |
curl (with c-ares) |
c-ares | Async DNS, similar to nslookup — bypasses getaddrinfo |
Check which curl you have:
curl --version | grep AsynchDNS
# AsynchDNS without c-ares = threaded getaddrinfo (libc)
# AsynchDNS with c-ares = c-ares (bypasses OS resolver)
curl is built with "AsynchDNS" and c-ares. Does it still go through the same OS resolver chain as a default curl build?
Debugging: curl works but nslookup fails
Four causes account for almost all of these, and they share one signature: the OS resolver chain that curl uses and the raw DNS query that nslookup sends are legitimately seeing different answers for the same hostname.
curl myservice expands to myservice.corp.internal via search domains (the search line in /etc/resolv.conf). nslookup myservice queries the bare name exactly as typed — no expansion at all.
# Test with the full name
nslookup myservice.corp.internal
curl → getaddrinfo() → 127.0.0.53 (the systemd-resolved stub, which has a cached answer) → works.
nslookup → queries upstream DNS directly → upstream is broken → fails.
# Force nslookup to use the stub resolver
nslookup myservice 127.0.0.53
# If this works, systemd-resolved is shielding curl from the broken upstream
Internal zone is not DNSSEC-signed. Upstream returns SERVFAIL when trying to validate. systemd-resolved has DNSSEC=allow-downgrade set, so curl is fine; nslookup queries upstream directly and gets the raw SERVFAIL.
# Disable DNSSEC validation for the query
dig myservice +cd +short
# If this works, DNSSEC is the issue
Most common cause. nslookup queries both A and AAAA records. Internal DNS handles A fine but returns SERVFAIL for AAAA (instead of the correct NOERROR + empty answer). curl's getaddrinfo() ignores the AAAA failure and just uses the working A record.
# Isolate which record type fails
nslookup -type=A myservice # should work
nslookup -type=AAAA myservice # will show SERVFAIL
Fix: configure the internal DNS zone to return NOERROR (empty answer) for AAAA queries.
Workaround:
curl -4 myservice # force IPv4
nslookup -type=A myservice # query only A record
Which of the four causes above is flagged as the most common in practice?
Quick diagnostic flow
graph TD
classDef start fill:#34495e,stroke:#2c3e50,color:#fff
classDef found fill:#f39c12,stroke:#ba6018,color:#fff
classDef bad fill:#e74c3c,stroke:#c0392b,color:#fff
A["curl works<br/>nslookup returns SERVFAIL"]:::start --> B{"nslookup -type=A myservice<br/>succeeds?"}
B -->|"Yes - only -type=AAAA fails"| C["IPv6/AAAA issue on internal DNS<br/>(most common - see tabs above)"]:::found
B -->|"No - both A and AAAA fail"| D{"nslookup myservice 127.0.0.53<br/>(the systemd-resolved stub) succeeds?"}
D -->|"Yes"| E["systemd-resolved is quietly hiding<br/>a broken upstream from curl"]:::found
D -->|"No"| F{"nslookup myservice.corp.internal<br/>(full FQDN) succeeds?"}
F -->|"Yes"| G["Search-domain expansion issue -<br/>curl expands the name, nslookup doesn't"]:::found
F -->|"No"| H{"dig myservice +cd<br/>(disable DNSSEC checking) succeeds?"}
H -->|"Yes"| I["DNSSEC validation failure<br/>on the internal zone"]:::found
H -->|"No"| J["Upstream DNS server itself is broken -<br/>none of the above narrowed it down"]:::bad
In the flowchart above, nslookup -type=A fails too — not just AAAA. What should you check next, in order?
Debugging: nslookup works but curl fails
This direction is less common but just as real: DNS is already proven fine — nslookup got a correct answer — yet curl still can't complete a request. Because DNS is no longer a suspect, the debugging path is layered rather than branching: work outward from the network stack toward the application, one layer at a time, and stop at the first layer that doesn't come back clean.
/etc/hosts, resolv.conf, or the resolver chain here — the layer that debugging usually starts at is already proven fine. Skip straight to the transport layer.
nc -zv host port or curl -v --connect-timeout 5 http://host:port and watch for a completed handshake. If the connection just hangs and times out with zero response, something is silently dropping the SYN — most often a security group or firewall rule doing DROP instead of REJECT, or nothing actually listening on that port.
openssl s_client -connect host:443 -servername host. A failure here — certificate mismatch, expired cert, wrong SNI, unsupported TLS version/cipher — shows up in curl as an "SSL certificate problem" or "SSL routines" error. DNS and TCP were never the issue.
Host header, a load balancer routing to the wrong backend, a WAF or rate limiter blocking the request, or the application itself erroring or hanging. curl -v shows exactly which of these stages it got stuck at — read the last line it printed before it failed.
graph TD
classDef ok fill:#27ae60,stroke:#1e8449,color:#fff
classDef warn fill:#f39c12,stroke:#ba6018,color:#fff
classDef bad fill:#e74c3c,stroke:#c0392b,color:#fff
classDef step fill:#34495e,stroke:#2c3e50,color:#fff
A["nslookup succeeds<br/>curl fails or hangs"]:::step --> B
subgraph LAYERS["Layer-by-layer isolation - DNS already ruled out"]
B{"curl -v --connect-timeout 5<br/>completes the TCP handshake?"}
D{"Is this HTTPS?"}
E{"openssl s_client -connect host:443<br/>completes the handshake?"}
G{"curl gets an HTTP response<br/>at all, any status code?"}
B -->|"Yes"| D
D -->|"Yes"| E
D -->|"No - plain HTTP"| G
E -->|"Yes"| G
end
B -->|"No - hangs, no SYN-ACK"| C["Firewall / security group dropping<br/>the SYN, or nothing listening on the port"]:::bad
E -->|"No - cert/SNI/TLS error"| F["TLS problem: expired or mismatched cert,<br/>wrong SNI, unsupported TLS version/cipher"]:::warn
G -->|"No - times out waiting on response"| H["Application/backend layer:<br/>server hung, slow, or misconfigured"]:::warn
G -->|"Yes, but 4xx/5xx or wrong body"| I["Application layer: wrong Host header,<br/>LB routing, WAF/rate limit, app bug"]:::bad
G -->|"Yes, 2xx as expected"| J["Working - the original failure<br/>was transient or already fixed"]:::ok
nslookup returns the correct IP instantly, but curl hangs and eventually times out with no TLS or HTTP error at all. Which layer is broken?
Key rule
If
curl hostworks butnslookup hostfails — the problem is in DNS resolution, not connectivity. The two tools traverse different resolver paths. Narrow down which path is broken.
If nslookup works but curl fails — DNS is fine; the issue is TCP, TLS, firewall, or the application itself. Work through the layered breakdown above rather than re-checking DNS a second time.