๐ŸŽ‰ Premium Proxies ยท 24-Hour Free TrialClaim Now
Proxy Basic

What Is DNS Resolution and How Proxies Handle It

How DNS resolution works, where it happens in a proxied request, and why socks5 vs socks5h and HTTP CONNECT decide whether your client or the proxy resolves.

S SparkProxy 2 19 min read
Share
What Is DNS Resolution and How Proxies Handle It

DNS resolution is the step that turns a hostname into an IP address, and it happens before your proxy ever sees a byte of your request. That ordering is the whole story. Depending on the protocol scheme and the library you picked, the lookup runs either on your machine or at the proxy, and those two paths produce different exit behaviour, different CDN edges, and different failure modes. This guide covers the resolver chain, where the lookup lands in each proxy protocol, the socks5 versus socks5h split that trips up most scrapers, and how caching and TTL keep stale answers alive longer than you expect.

What DNS Resolution Actually Does

DNS resolution is the process of asking the Domain Name System for the records attached to a name, most commonly the A record (IPv4 address) or AAAA record (IPv6 address), so a client can open a TCP connection to a numeric address. The system is defined in RFC 1034 and RFC 1035, published in 1987, and the core query format has barely changed since.

Nothing about the web works without it. https://www.sparkproxy.io/ is not routable. 203.0.113.10:443 is. A browser, a curl call, and a Python scraper all have to convert the first into the second before a socket exists.

Two properties matter for anyone working with proxies:

  • The lookup is a separate network transaction. It uses its own packets, usually UDP on port 53, and it goes wherever the resolving host's network configuration points. It does not automatically follow your HTTP traffic.
  • The result is cached, aggressively, at multiple layers. The answer you get is often not fresh. It is whatever some cache still considers valid.

Both properties are why DNS shows up in proxy discussions at all. A lookup that runs on your machine reveals what you are about to visit to your ISP resolver, and it resolves against your location rather than the proxy's.


The Resolver Chain, Step by Step

A cold lookup for shop.sparkproxy.io walks a hierarchy:

  1. Stub resolver. Your OS library (getaddrinfo on Linux and macOS, the DNS Client service on Windows) checks its own cache and the hosts file, then forwards the question to a configured recursive resolver.
  2. Recursive resolver. Your ISP's resolver, or a public one like 1.1.1.1 or 9.9.9.9, or a resolver running inside your own network. It does the real work and keeps the largest cache.
  3. Root nameservers. Thirteen root server identities, named a.root-servers.net through m.root-servers.net and served by hundreds of anycast instances. They answer "I don't know, ask the .io servers."
  4. TLD nameservers. The .io registry servers answer "ask the nameservers listed for sparkproxy.io."
  5. Authoritative nameservers. These hold the zone and return the actual A record for shop.sparkproxy.io, with a TTL attached.

Each answer is cached on the way back. A warm lookup usually stops at step 1 or 2 and returns in under a millisecond. A genuinely cold recursion across all five levels can take 100 ms to 300 ms.

LayerTypical latencyCache lifetimeWho sees the query
Process or browser cacheunder 1 ms60 s in Chrome, varies elsewherenobody
OS stub cacheunder 1 msrecord TTLnobody
Recursive resolver1 ms to 30 msrecord TTLISP or resolver operator
Root, TLD, authoritative100 ms to 300 msgoverned by TTL and SOAroot, TLD, and zone operators

That third row is the privacy-relevant one. Whoever runs the recursive resolver sees a plaintext record of every hostname you look up, unless you are using DNS over HTTPS or DNS over TLS.


Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Where DNS Sits in a Proxied Request

Here is the sequence for an unproxied HTTPS request:

1. resolve api.sparkproxy.io  ->  203.0.113.10
2. TCP connect 203.0.113.10:443
3. TLS handshake (SNI: api.sparkproxy.io)
4. HTTP request

Now add a proxy. The client has to reach the proxy first, so a lookup for the proxy's own hostname always happens locally:

1. resolve gateway.sparkproxy.io  ->  198.51.100.7     (always local)
2. TCP connect 198.51.100.7:11000
3. ??? resolve api.sparkproxy.io                       (local OR at the proxy)
4. tunnel or relay to api.sparkproxy.io:443

Step 3 is the only variable, and everything in this article is about which side owns it. Step 1 is not negotiable. Even a perfectly configured setup performs one local DNS lookup for the proxy gateway itself, which is why "route all DNS through the proxy" is never literally true. If you want to remove even that lookup, connect to the gateway by IP.


Local vs Remote Resolution

Local resolution means your client resolves the destination hostname and then hands the proxy an IP address. Remote resolution means your client hands the proxy a hostname, and the proxy resolves it against a resolver near the exit node.

Local resolutionRemote resolution
Who queries DNSyour machinethe proxy
Your resolver sees target hostnamesyesno
Resolves againstyour geographyexit node geography
Works with split-horizon DNS at the exitnoyes
Extra round trip on the clientyesno, folded into the proxy handshake
Blocked by local DNS filteringyesno
Common name for the failureDNS leaknot applicable

The practical consequences show up in three places. Split-horizon DNS is one: if a target resolves differently inside the exit network, only remote resolution gets the right answer. Geo-sensitive CDN routing is the second, covered further down. The third is DNS-level blocking, where a local resolver returns NXDOMAIN or a sinkhole address for a domain that resolves fine from the exit.

For the full detection procedure and the remediation checklist, see the companion piece on proxy DNS leak testing and mitigation. This article stays on the mechanics of who resolves what.


How HTTP Proxies Handle DNS

HTTP proxies have two request modes, and they arrive at the same DNS outcome by different routes.

Absolute-form requests (plain HTTP).

For an http:// URL, the client sends the full URL in the request line instead of a path:

GET http://api.sparkproxy.io/v1/status HTTP/1.1
Host: api.sparkproxy.io
Proxy-Authorization: Basic dXNlcjpwYXNz

The client never needs the destination IP. It sends a hostname, and the proxy resolves it. Resolution is remote by construction.

CONNECT tunnels (HTTPS and everything else).

For https://, the client asks for a tunnel first:

CONNECT api.sparkproxy.io:443 HTTP/1.1
Host: api.sparkproxy.io:443

The destination is a hostname and a port. The proxy resolves the name, opens a TCP connection, and replies 200 Connection Established, after which it relays bytes without reading them. The mechanics of that relay are covered in what an HTTP tunnel is.

This is the detail most guides skip: an HTTP proxy defers destination DNS to the proxy in both modes, with no configuration flag involved. There is no httph:// scheme because there does not need to be one. HTTP proxying is remote-DNS by default, which makes it the safer choice when you have not audited your client's resolver behaviour. The trade-offs against the other protocol are laid out in SOCKS5 vs HTTP proxies.

Two caveats keep this from being absolute:

  • DNS over HTTPS in browsers. If Chrome or Firefox has DoH enabled, the browser resolves through an HTTPS request to a DoH provider. Behind an HTTP proxy that request is itself a CONNECT, so it stays tunnelled, but the resolution happens at the browser's chosen provider rather than at the exit node.
  • Applications that pre-resolve. Some clients resolve a hostname for their own logic (connection pooling keyed by IP, custom SNI, IP pinning) and then send CONNECT 203.0.113.10:443. The proxy is handed an IP, and the lookup already happened on your machine.

How SOCKS Proxies Handle DNS: socks5 vs socks5h

SOCKS is where the real footgun lives. RFC 1928, published in 1996, defines an address type byte (ATYP) in the SOCKS5 request:

ATYP valueMeaningWho resolved
`0x01`IPv4 address, 4 bytesthe client
`0x03`Domain name, length-prefixedthe proxy
`0x04`IPv6 address, 16 bytesthe client

A SOCKS5 proxy is perfectly capable of remote resolution. Whether it gets the chance depends entirely on which ATYP your client sends, and most clients default to 0x01.

The socks5h scheme exists to make the choice explicit. It is not a different protocol. It is a client-side URL convention, popularised by curl, where the trailing h means "let the proxy resolve the hostname":

  • socks5:// sends ATYP 0x01. The client resolves. DNS is local.
  • socks5h:// sends ATYP 0x03. The proxy resolves. DNS is remote.

The same split exists one version back. SOCKS4 has no domain-name address type at all, so it is local-only, while the SOCKS4a extension added one. The scheme naming follows: socks4 is local, socks4a is remote. That version history is covered in SOCKS4 vs SOCKS5 proxies, and the protocol itself in what a SOCKS proxy is.

In curl, the flags and schemes line up like this:

# Local DNS: curl resolves, then sends an IPv4 address in the SOCKS request
curl --proxy socks5://user:pass@gateway.sparkproxy.io:13000 https://api.sparkproxy.io/v1/status

# Remote DNS: curl sends the hostname, the proxy resolves it
curl --proxy socks5h://user:pass@gateway.sparkproxy.io:13000 https://api.sparkproxy.io/v1/status

# Equivalent flag form of the line above
curl --socks5-hostname user:pass@gateway.sparkproxy.io:13000 https://api.sparkproxy.io/v1/status

One letter changes where the lookup runs. Nothing in the response tells you which one you got, which is exactly why this misconfiguration survives in production for months.


What Your Library Actually Does

Defaults vary by ecosystem, and several of them contradict what developers assume. Verify rather than trust.

ClientHTTP proxySOCKS5 defaultHow to force remote DNS
curl 8.xremotelocal`socks5h://` or `--socks5-hostname`
Python `requests` with PySocksremotelocal`socks5h://` in the proxies dict
Python `httpx` with `httpx-socks`remotedepends on transport`rdns=True` on the transport
Node `socks-proxy-agent`remotelocal`socks5h://` or the bare `socks://` scheme
Go `golang.org/x/net/proxy`remoteremote for non-IP hostsalready the default
Java `Proxy.Type.SOCKS`remotelocal`InetSocketAddress.createUnresolved(...)`
Chromium and Playwrightremoteremotedefault for `socks5://` in Chromium

Python is the most common place this bites, because the working and broken versions differ by a single character:

import requests

# WRONG for anonymity work: PySocks resolves api.sparkproxy.io on your machine
proxies = {
    "http": "socks5://user:pass@gateway.sparkproxy.io:13000",
    "https": "socks5://user:pass@gateway.sparkproxy.io:13000",
}

# RIGHT: the hostname is sent to the proxy and resolved at the exit
proxies = {
    "http": "socks5h://user:pass@gateway.sparkproxy.io:13000",
    "https": "socks5h://user:pass@gateway.sparkproxy.io:13000",
}

r = requests.get("https://api.sparkproxy.io/v1/status", proxies=proxies, timeout=20)
print(r.status_code)

socks5h support requires the SOCKS extra, installed with pip install "requests[socks]", which pulls in PySocks. Without it, requests raises InvalidSchema: Missing dependencies for SOCKS support rather than falling back silently. That is at least an honest failure.

Node behaves the same way, with the scheme driving an internal lookup flag:

import { SocksProxyAgent } from 'socks-proxy-agent';

// socks5:// resolves locally; the trailing h moves the lookup to the proxy
const agent = new SocksProxyAgent('socks5h://user:pass@gateway.sparkproxy.io:13000');

const res = await fetch('https://api.sparkproxy.io/v1/status', { dispatcher: agent });
console.log(res.status);

Go's golang.org/x/net/proxy SOCKS5 dialer is the pleasant exception. Its Dial method takes a host:port string and writes ATYP 0x03 whenever the host is not already an IP literal, so remote resolution is the default with no special scheme:

dialer, err := proxy.SOCKS5("tcp", "gateway.sparkproxy.io:13000", &proxy.Auth{
    User: "user", Password: "pass",
}, proxy.Direct)
if err != nil {
    log.Fatal(err)
}

// api.sparkproxy.io is sent as a domain name, resolved at the proxy
conn, err := dialer.Dial("tcp", "api.sparkproxy.io:443")

DNS Caching and TTL

Every record carries a TTL in seconds, set by the zone operator, and every cache along the chain may keep the answer for that long. Typical values in the wild:

Record typeCommon TTLWhy
CDN-fronted `A` or `CNAME`20 s to 60 sfast failover and load steering
Ordinary site `A` record300 s to 3600 sbalance of freshness and query volume
`MX` and `TXT`3600 s to 86400 srarely change
`NS` delegation86400 s or morevery stable
Negative answers such as `NXDOMAIN`SOA `MINIMUM`, capped at 3 hoursRFC 2308

That last row causes more scraper outages than any other DNS behaviour, and almost nobody instruments it. Under RFC 2308, published in 1998, a negative answer is cached using the smaller of the SOA MINIMUM field and the SOA record's own TTL, with implementations capping the result at three hours. If a target's nameservers glitch for thirty seconds and your resolver caches an NXDOMAIN with a 900-second negative TTL, your crawler keeps failing for fifteen minutes after the target recovered. The logs say Name or service not known and the target looks down. It isn't.

Long-lived processes make this worse. A Python worker that runs for days accumulates resolutions in whatever caching sits beneath getaddrinfo, and some HTTP clients keep connection pools keyed to an IP that has since been rotated out of the CDN. If you rotate exit IPs while the destination IP stays frozen in a pool, you are hitting one edge server with every identity you own.

Inspect and clear the caches you control:

# Linux with systemd-resolved
resolvectl statistics
resolvectl flush-caches

# macOS
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder

# Windows
ipconfig /displaydns
ipconfig /flushdns

# Query the authoritative answer and its TTL directly, bypassing local caches
dig +noall +answer @1.1.1.1 api.sparkproxy.io A

Chrome keeps its own short-lived cache on top of the OS, visible at chrome://net-internals/#dns. If you are testing proxy behaviour in a browser, clear that too, or you will credit a cached answer to your new proxy config.

The operational rule: when DNS matters to your pipeline, cap your own cache below the record TTL and never cache negative answers for more than a minute. Most HTTP clients accept a custom resolver or an explicit DNS cache TTL. Set it deliberately instead of inheriting a three-hour negative cache from the OS.


CDN Edges, EDNS Client Subnet, and Geo Mismatch

Here is the part that generic DNS explainers miss entirely, and it explains a class of proxy bug that usually gets misdiagnosed as "the provider's geo-targeting is broken."

Large CDNs answer DNS queries differently depending on where the query appears to come from. The mechanism is EDNS Client Subnet, defined in RFC 7871 (2016), which lets a recursive resolver attach a truncated prefix of the client's IP, commonly a /24 for IPv4, to the upstream query. The authoritative nameserver then returns the edge closest to that prefix rather than the edge closest to the resolver.

Now trace three configurations, all using a German exit node:

  1. Local DNS. Your resolver in Ohio attaches your Ohio prefix. The CDN hands back a Chicago edge IP. Your request then travels through the German proxy to a Chicago edge server. You get US-flavoured content from a German IP, along with a latency profile that makes no sense, and the pages you scrape may not match what a German visitor sees.
  2. Remote DNS, resolver near the exit. The lookup happens at the exit, the CDN sees a German prefix, and you get a Frankfurt edge. Content and routing agree.
  3. Remote DNS, but the provider uses one centralised resolver. The proxy resolves remotely, so there is no leak, yet the resolver sits in the provider's home region. The CDN steers you to an edge near that resolver, not near your exit. No leak, wrong edge.

Configuration 3 is the subtle one. It passes every DNS leak test, because a leak test only checks whose resolver made the query, and it still returns geographically inconsistent content. If you are doing localisation checks or regional price monitoring, verify the edge IP you actually connected to, not just the resolver. This connects directly to how proxy geo-targeting is supposed to work, and to the broader relationship between IP addresses and proxies.

A quick check, run from inside your proxied environment:

# Which edge did we actually reach? Compare the result across exit countries.
curl -s -x http://user:pass@gateway.sparkproxy.io:11000 \
  -o /dev/null -w '%{remote_ip}\n' https://api.sparkproxy.io/v1/status

If the same target resolves to the same edge IP from a German exit and a Japanese exit, resolution is not following your exit geography.


DNS With the SparkProxy Scraping API

Everything above is a client-side problem, and it disappears when the client never resolves anything. With the SparkProxy Scraping API you send the target URL as a parameter. The only hostname your machine resolves is scrape.sparkproxy.io. The target hostname is resolved server-side, next to the exit node that fetches it.

curl -X GET "https://scrape.sparkproxy.io/api/v1?url=https://example.sparkproxy.io/pricing&render_js=true" \
  -H "X-API-Key: YOUR_API_KEY"

Add geo-targeting and the resolution follows the exit region rather than yours:

curl -X GET "https://scrape.sparkproxy.io/api/v1?url=https://example.sparkproxy.io/pricing&render_js=true&premium_proxy=true&country_code=de&format=md" \
  -H "X-API-Key: YOUR_API_KEY"

The same request in Python, with the JSON envelope so you can log status and timing:

import requests

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    params={
        "url": "https://example.sparkproxy.io/pricing",
        "render_js": "true",
        "premium_proxy": "true",
        "country_code": "de",
        "json_response": "true",
    },
    headers={"X-API-Key": "YOUR_API_KEY"},
    timeout=120,
)

data = resp.json()
print(data["status_code"], data["duration_ms"], data["credits_used"])

There is no socks5h decision to get wrong here, no resolver to audit, and no local negative cache to poison your retries. If your requirement is "the target must never be looked up from my network", the API removes the failure mode instead of asking you to configure around it. If you would rather keep control of the client, use an HTTP proxy with CONNECT or a SOCKS5 proxy with socks5h, and then test it. Questions on either path go to support@sparkproxy.io.


Checking Which Side Resolved

Three checks, in increasing order of confidence:

1. Read the wire. Watch port 53 on your own machine while you make a proxied request. Any query for the target hostname means local resolution.

sudo tcpdump -n -i any port 53

2. Read the client's own trace. curl says exactly what it did:

curl -v --proxy socks5h://user:pass@gateway.sparkproxy.io:13000 https://api.sparkproxy.io/ 2>&1 | head -8
# SOCKS5 communication to api.sparkproxy.io:443   <- hostname sent, resolution is remote

With plain socks5://, the same trace shows a resolved IP address in place of the hostname.

3. Use a resolver-reflection service. A DNS leak test embeds a unique subdomain that can only resolve by contacting a nameserver the test controls, then reports which resolver IP asked for it. That is the authoritative answer to "whose resolver ran the query." The full method, including what to do about a positive result, is in the proxy DNS leak testing guide.

Run check 2 in CI. It costs one request, and it catches the day someone edits a config file and drops the h.


Frequently asked questions

FAQ

Not automatically. An HTTP proxy resolves the destination for you in both absolute-form and CONNECT mode, so destination lookups stay off your network. A SOCKS5 proxy only performs remote DNS resolution if the client sends a domain name rather than an IP, which in practice means using the socks5h scheme. In every case, the lookup for the proxy's own hostname happens locally.

Use socks5h whenever the destination lookup must not run on your machine, which covers scraping, geo-sensitive checks, and anything privacy-related. Both schemes speak the same protocol: socks5:// resolves locally and sends an IP to the proxy, while socks5h:// sends the hostname so the proxy resolves it at the exit. Stay on plain socks5 only when you deliberately want to pin a specific destination IP.

For the destination hostname, no, because the proxy receives a name in both request modes and resolves it itself. Leaks still happen when the application pre-resolves and sends CONNECT 203.0.113.10:443, when a browser uses its own DNS over HTTPS provider, or when WebRTC opens a separate path outside the proxy.

The lookup for the proxy gateway comes first, always, and it is local. The destination lookup happens after your client has connected to the proxy, either on your machine just before the SOCKS request is built, or at the proxy once it receives the hostname. That ordering is why one local DNS query is unavoidable.

As long as its TTL allows, typically 20 to 60 seconds for CDN-fronted records and 300 to 3600 seconds for ordinary A records. Negative answers such as NXDOMAIN are cached separately under RFC 2308, using the SOA MINIMUM value and capped at three hours, which is why a brief nameserver failure can keep a crawler broken long after the target recovered.

Barely, and it often helps. The lookup moves into the proxy handshake instead of adding a separate client-side round trip, and the proxy's resolver is usually warm for popular targets. Any small cost is offset by getting the CDN edge that matches your exit region rather than one picked for your own location.


Special Discount ยท 20% off

Get 20% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Save up to 15% more on quarterly, half-yearly and yearly plans

Claim Discount

About the Author

The SparkProxy Technical Team builds and operates SparkProxy's datacenter, residential, and ISP proxy networks along with the SparkProxy Scraping API. We work on resolver placement, exit-node routing, and the client-side integration problems that turn up in real scraping pipelines, and we write these guides from what the network and the support queue actually show us. Full API parameters are documented at sparkproxy.io/docs/scraping-api, and technical questions reach us at support@sparkproxy.io.

Keep reading

Related articles

Why Antidetect Browsers Need Proxies

Why Antidetect Browsers Need Proxies

Why antidetect browsers need proxies: the browser controls what a page reads, the proxy controls where packets come from. Detection scores both layers.

SparkProxyยทProxy Basic