The "h" in socks5h:// tells the client to send the proxy the site's name instead of looking up its IP address itself. Without it, the DNS lookup leaves your machine and goes to your own resolver, which then knows every site you open through the proxy.
The scheme is only half the story, though: what happens with plain socks5:// depends on the client. In October 2026 we ran 22 HTTP clients, libraries and browsers through our own SOCKS5 server, three rounds each, and logged what every one of them sent. 8 of the 22 resolved the name locally with socks5://. The other 14, including every browser we tried, passed the name to the proxy even without the "h".
Short answer
- curl, PHP cURL, Python requests and urllib3, Node.js socks-proxy-agent (with http, axios or got) and the proxy-chain relay leak DNS with
socks5://. Writesocks5h://and the leak is gone in every one of them. - Python httpx, aiohttp-socks, Go net/http and Node undici with fetch-socks send the name to the proxy with plain
socks5://. aiohttp-socks does not acceptsocks5h://at all and fails with a ValueError. - Chrome, Chromium, Puppeteer, Playwright, Firefox and Camoufox did not leak. In Firefox the leak appears only if you switch off
network.proxy.socks5_remote_dns. - proxychains-ng is safe with its default config, which has
proxy_dnsswitched on. Remove that line and it leaks.
How to fix it: settings that send the name to the proxy
Every snippet below was run against our test server with a username and password, and in every case the proxy received the site's name, not an IP. Replace user:pass@host:port with your proxy's details.
curl
curl -x socks5h://user:pass@host:port https://example.com
curl --socks5-hostname host:port --proxy-user user:pass https://example.com
export ALL_PROXY=socks5h://user:pass@host:port
PHP cURL
$ch = curl_init("https://example.com");
curl_setopt($ch, CURLOPT_PROXY, "host:port");
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, "user:pass");
A proxy URL works too: CURLOPT_PROXY set to socks5h://user:pass@host:port.
Python requests and urllib3
pip install "requests[socks]"
import requests
proxy = "socks5h://user:pass@host:port"
requests.get("https://example.com", proxies={"http": proxy, "https": proxy})
from urllib3.contrib.socks import SOCKSProxyManager
http = SOCKSProxyManager("socks5h://user:pass@host:port")
Python httpx and aiohttp
Both already send the name with socks5://. Keep that scheme: aiohttp-socks fails on socks5h://.
pip install "httpx[socks]" aiohttp-socks
import httpx
client = httpx.Client(proxy="socks5://user:pass@host:port")
from aiohttp_socks import ProxyConnector
connector = ProxyConnector.from_url("socks5://user:pass@host:port")
# aiohttp.ClientSession(connector=connector)
Node.js: axios, got, http
import axios from "axios"
import got from "got"
import { SocksProxyAgent } from "socks-proxy-agent"
const agent = new SocksProxyAgent("socks5h://user:pass@host:port")
await axios.get("https://example.com", { httpAgent: agent, httpsAgent: agent, proxy: false })
await got("https://example.com", { agent: { http: agent, https: agent } })
Puppeteer or Playwright with a SOCKS5 login, through proxy-chain
import puppeteer from "puppeteer"
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain"
const proxyUrl = await anonymizeProxy("socks5h://user:pass@host:port")
const browser = await puppeteer.launch({ args: [`--proxy-server=${proxyUrl}`] })
// ...
await browser.close()
await closeAnonymizedProxy(proxyUrl, true)
Firefox
On by default. If a profile has it switched off, set it back in about:config or in the profile's user.js:
user_pref("network.proxy.socks5_remote_dns", true);
proxychains-ng
Make sure proxy_dns is in the config and not commented out:
strict_chain
proxy_dns
[ProxyList]
socks5 host port user pass
Go and browsers
Nothing to change. Go's net/http, Chrome, Chromium, Puppeteer without a relay, Playwright and Camoufox sent the name with plain socks5:// in our test.
What the "h" changes
When a client asks a SOCKS5 proxy to open a connection, it names the destination in one of two ways: as an IP address or as a domain name (RFC 1928 calls these address types). With a domain name the proxy looks up the address on its side. With an IP address the lookup has already happened on your machine, through your network's DNS resolver.
socks5h is curl's way of saying "send the name", and Python and Node libraries copied it. The protocol on the wire is the same SOCKS5 in both cases, only the address type in that one request differs.
How we tested
We wrote a small SOCKS5 server that records what each client puts into its connection request: a name or an IP. Every test opened its own unique hostname of the form <test>-<random>.127.0.0.1.sslip.io. Our server read the target address straight from that name without any DNS lookup, so any DNS query for the name that showed up in our packet capture could only have come from the client.
Each client opened the page once per round, three rounds in a row, on October 21, 2026. Every client and setting gave the same result in all three rounds.

The leaking queries did not stay on the machine. With plain socks5:// in curl, the A and AAAA queries for the test name went out to the DNS resolver of our hosting provider, a party that is not the proxy and has no reason to see which sites you open through it.

Results: who sends the name to the proxy
| Client | What we set | Sent to the proxy | Local DNS lookup |
|---|---|---|---|
| curl | -x socks5://, --socks5, ALL_PROXY=socks5:// | IP | Yes |
| curl | -x socks5h://, --socks5-hostname | Name | No |
| PHP cURL | CURLOPT_PROXY socks5://, CURLPROXY_SOCKS5 | IP | Yes |
| PHP cURL | CURLOPT_PROXY socks5h://, CURLPROXY_SOCKS5_HOSTNAME | Name | No |
| Python requests | socks5:// | IP | Yes |
| Python requests | socks5h:// | Name | No |
| Python urllib3 | socks5:// | IP | Yes |
| Python urllib3 | socks5h:// | Name | No |
| Python httpx | socks5:// or socks5h:// | Name | No |
| Python aiohttp + aiohttp-socks | socks5:// | Name | No |
| Python aiohttp + aiohttp-socks | socks5h:// | Error: scheme not accepted | |
| Go net/http | socks5:// or socks5h:// | Name | No |
| Node.js http, axios, got + socks-proxy-agent | socks5:// | IP | Yes |
| Node.js http, axios, got + socks-proxy-agent | socks5h:// | Name | No |
| Node.js undici + fetch-socks | type 5 | Name | No |
| proxy-chain (Apify) | socks5:// with login | IP | Yes |
| proxy-chain (Apify) | socks5h:// with login | Name | No |
| proxychains-ng | default config (proxy_dns) | Name | No |
| proxychains-ng | without proxy_dns | IP | Yes |
| Google Chrome | --proxy-server=socks5:// | Name | No |
| Chromium, Puppeteer | --proxy-server=socks5:// | Name | No |
| Playwright: Chromium, Firefox, WebKit | proxy: { server: 'socks5://…' } | Name | No |
| Firefox, Firefox ESR | SOCKS v5 in connection settings | Name | No |
| Firefox | network.proxy.socks5_remote_dns = false | IP | Yes |
| Camoufox | proxy: socks5:// | Name | No |
Versions: curl 8.5.0, PHP 8.5.12 with libcurl 8.22.0, Python 3.15 with requests 2.34.2, urllib3 2.8.0, httpx 0.28.1, aiohttp 3.14.5 and aiohttp-socks 0.12.0, Go 1.27.3, Node.js 24.22 with socks-proxy-agent 10.1.0, axios 1.20.0, got 16.1.0, undici 8.12.3, fetch-socks 1.3.3 and proxy-chain 3.0.1, proxychains-ng 4.17, Google Chrome 156, Firefox 158 and Firefox ESR 153, Playwright 1.62.1, puppeteer-core 25.15.0, Camoufox 0.5.8.
curl and everything built on libcurl
curl is where socks5h comes from, and it is the clearest case. -x socks5://, the --socks5 flag and an ALL_PROXY=socks5://… environment variable all made curl look up the name itself and send the proxy an IP. -x socks5h:// and --socks5-hostname sent the name.
PHP's cURL extension uses the same library and behaves the same way. In PHP code the proxy type is often set with a constant rather than a URL, so the fix there is CURLPROXY_SOCKS5_HOSTNAME instead of CURLPROXY_SOCKS5.
Python: depends on the library
requests and urllib3 go through PySocks and follow the curl convention: socks5:// resolved locally on every run, socks5h:// sent the name. This is the most common setup in scraping code and the one most tutorials copy.
httpx and aiohttp-socks sent the name even with socks5://. The catch is in the other direction: aiohttp-socks rejects socks5h:// with ValueError: Invalid scheme component, so a proxy string that is correct for requests breaks an aiohttp scraper. Keep socks5:// there, it already resolves on the proxy side.
Go and Node.js
Go's net/http sent the name with both socks5:// and socks5h://, so Go programs that take the proxy from a URL or from HTTP_PROXY are safe either way.
In Node.js, socks-proxy-agent is the usual way to put a SOCKS5 proxy under the built-in http module, axios or got, and it follows curl: socks5:// resolved locally, socks5h:// sent the name. undici with fetch-socks, which takes the proxy as an object rather than a URL, sent the name.
Browsers did not leak
Every browser we ran sent the name to the proxy: Google Chrome and Chromium with --proxy-server=socks5://, Puppeteer, all three Playwright engines, Firefox and Firefox ESR with SOCKS v5 set in the connection settings, and Camoufox. Chromium under Puppeteer also routed its own background requests, such as the ones to Google accounts, through the proxy by name.
Firefox has two settings with similar names, and many guides still point to the wrong one. network.proxy.socks_remote_dns no longer changes anything for SOCKS5: we set it to false and Firefox still sent the name. The setting that matters is network.proxy.socks5_remote_dns, which is on by default. Switching it off was the only way we got Firefox to leak.
Browsers driven by Playwright and Camoufox refused a SOCKS5 proxy with a username and password: the launch failed with "Browser does not support socks5 proxy authentication". Most rotating residential and mobile proxies require a login, which leads to the next problem.
The trap: a relay for SOCKS5 with a password
The common way around that limit is a local relay that accepts the browser's connection without a password and forwards it to the SOCKS5 proxy with one. The popular choice for Puppeteer and Playwright is proxy-chain from Apify.
Given socks5://user:pass@host:port, proxy-chain looked up the name itself and passed the proxy an IP, even though the browser behind it had sent the name. Given socks5h://user:pass@host:port, it passed the name. So a browser that never leaks on its own starts leaking the moment you put this relay in front of it with the wrong scheme.
proxychains-ng
proxychains-ng, which forces any program through a proxy, ships with proxy_dns switched on in its default config, and with it the name went to the proxy. Configs copied from old guides sometimes leave that line out or comment it out. Without it, the program behind proxychains resolved the name locally.
Why a local lookup matters
A local lookup goes to the resolver of your own network, so that resolver sees every site you open through the proxy, and a DNS leak test shows your real resolver next to the proxy's IP.
The lookup also runs from your location: a site behind a CDN may hand your machine the address of a server near you rather than near the proxy, and the request then reaches a server picked for the wrong country.
How to check your own setup
For curl, compare a DNS leak test page opened with -x socks5:// and with -x socks5h://: the resolvers it lists should differ. For a library, the reliable check is the one we used: point the client at a SOCKS5 server you control and log whether the connection request carries a name or an IP. If you only need to see the proxy's exit IP and location, our proxy checker does that in the browser.
For the other differences between SOCKS5 and HTTP proxies, including speed, UDP and WebRTC, see our test of SOCKS5 vs HTTPS proxies on 11 real proxies.
FAQ
- What is the difference between socks5 and socks5h?
- With socks5h the client sends the proxy the site's name and the proxy looks up the address. With socks5, curl and the libraries that copy its convention look up the address on your own machine and send the proxy only the IP, so your DNS resolver sees every site you open. Some clients send the name even with socks5: in our test of 22 clients that was httpx, aiohttp-socks, Go, undici with fetch-socks and every browser.
- Does socks5:// leak DNS?
- In 8 of the 22 clients we tested, yes: curl, PHP cURL, Python requests and urllib3, Node.js socks-proxy-agent with http, axios and got, and the proxy-chain relay. In all of them socks5h:// stopped the leak.
- Is socks5h a different protocol?
- No. It is the same SOCKS5. The h only tells the client to put the domain name, not an IP address, into its connection request. Any SOCKS5 proxy that accepts domain names works with it.
- Do browsers leak DNS through a SOCKS5 proxy?
- Not in our test. Google Chrome, Chromium, Puppeteer, Playwright with Chromium, Firefox and WebKit, Firefox, Firefox ESR and Camoufox all sent the name to the proxy. Firefox leaked only after we switched off network.proxy.socks5_remote_dns.
- Which Firefox setting controls remote DNS for SOCKS5?
- network.proxy.socks5_remote_dns, on by default. The older network.proxy.socks_remote_dns that many guides mention did not change anything for SOCKS5 in Firefox 158 and Firefox ESR 153.
- Why does aiohttp reject socks5h://?
- aiohttp-socks does not know the socks5h scheme and raises ValueError: Invalid scheme component. Use socks5:// with it: in our test it already sent the name to the proxy.
- How do I use a SOCKS5 proxy with a password in Puppeteer or Playwright without a DNS leak?
- Chromium, Firefox and WebKit under Playwright do not accept a SOCKS5 login, so people put a local relay such as proxy-chain in front. Give the relay socks5h://user:pass@host:port. With socks5:// proxy-chain looked up names itself and sent the proxy IP addresses.