Using a proxy with curl is where most people meet proxies for the first time. It happens mid-debugging, with a -x flag copied off a forum. This guide is the reference we wanted that day. It covers every proxy flag that matters, the three gotchas that eat an afternoon each, and tested recipes for whole proxy lists.
Everything below is copy-paste runnable. Where an example needs a live proxy, we pull one from our free proxy API. It returns recently verified endpoints without a key.
How do you use a proxy with curl?
Pass the proxy to curl with the -x flag: curl -x http://host:port https://example.com. Add -U user:pass for authentication. Switch the scheme to socks5h:// for SOCKS5 with remote DNS. Or set the lowercase http_proxy variable to proxy every request in a script.
Your machine
your own address
Proxy
-x flag
Target
sees the exit IP
How many curl users run a proxy?
curl's author asks users every year which features they use. In every survey since 2022, about a third of the people who answered went through an HTTP proxy. SOCKS use climbed from a sixth to a quarter. The HTTPS proxy, which encrypts the link to the proxy itself, nearly doubled in 2025.
- HTTP proxy
- SOCKS proxy
- HTTPS proxy
The survey has a bias it states itself: it reaches "a small subset of users who voluntarily decide to fill in the questionnaire". Even so, proxies rank among the most used features in every edition, behind HTTP/2.
The basic flag: -x
One flag does almost everything:
curl -x http://203.0.113.7:8080 https://example.com/
-x (long form --proxy) takes a proxy URL: scheme, host, port. If you omit the scheme, curl assumes an HTTP proxy. If you omit the port, it assumes 1080, a SOCKS default that surprises people who expected 8080, so just always write the port.
The scheme is where the real decisions live:
| Scheme | Meaning |
|---|---|
http:// | HTTP proxy. Plain requests are forwarded. HTTPS is tunneled with CONNECT. |
https:// | The connection to the proxy itself is TLS. That differs from proxying an https:// URL. |
socks4:// | SOCKS4. TCP only, IPv4 only, DNS resolved on your machine. |
socks4a:// | SOCKS4a. Like SOCKS4, but the proxy resolves hostnames. |
socks5:// | SOCKS5. DNS resolved locally. |
socks5h:// | SOCKS5. DNS resolved by the proxy (the h is for hostname). |
Two of these hide traps worth spelling out.
What the proxy actually receives
We wrote a proxy that records what it is given, ran curl 8.16.0 through it on September 16, 2026, and read the log. Everything below is from that run, and both proxy and target were stubs on this machine:
| What we ran | What the proxy received | What the target received |
|---|---|---|
--proxy http://p with a plain URL | GET http://target/ in full. | The request. |
the same with --proxytunnel | CONNECT target:port. | The request, through the tunnel. |
-U user:pass | Proxy-Authorization: Basic ... | Nothing about the login. |
http://user:pass@p | The identical header. | Nothing about the login. |
--proxy-header 'X-Note: ...' | The header. | The header as well. |
the same with --proxytunnel | The header, in the CONNECT. | Nothing. |
Two of those rows are worth keeping. Both ways of writing credentials put the same bytes on the wire. The choice is only about where the password gets written down. And a proxy header stays private only inside a tunnel. Without one there is a single request, the proxy forwards it, and the target sees whatever you attached. Add --proxytunnel if the header is meant for the proxy alone.
Gotcha 1: socks5 vs socks5h
With socks5://, your machine performs the DNS lookup and sends the proxy an IP address. Your local resolver and network see every hostname you visit. If the name only resolves inside the proxy's network, the request fails entirely. With socks5h://, the hostname travels to the proxy and resolution happens there. SOCKS5 (RFC 1928) lets a request carry either a raw IP or a domain name. socks5h simply tells curl to send the name.
# DNS resolved locally: your resolver sees "httpbin.org"
curl -x socks5://198.51.100.14:1080 https://httpbin.org/ip
# DNS resolved by the proxy: your resolver sees nothing
curl -x socks5h://198.51.100.14:1080 https://httpbin.org/ip
If you use SOCKS5 for privacy, socks5h is almost always what you meant. We covered why in the SOCKS5 explainer.
Privacy is not the only reason. In our own run the difference decided whether the request worked at all. Our stub proxy recorded the address type of each request, which is the byte RFC 1928 uses to say name or address:
socks5://and--socks5: curl resolved the name itself, and the proxy received an address. Forlocalhostthat address was the IPv6 one, our stub target was listening on IPv4, and the proxy answered reply 1. The command failed with exit 97.socks5h://and--socks5-hostname: the proxy received the name, resolved it on its own side, and the request succeeded.
Same proxy, same target, one letter different. Local resolution can hand the proxy something it cannot use, so socks5h is the safer default even when nobody is watching your DNS.
Gotcha 2: https:// proxy scheme vs proxying HTTPS URLs
With -x https://proxy:port, curl speaks TLS to the proxy itself. Few proxies support that, mostly modern commercial gateways. A normal HTTP proxy carries your HTTPS traffic through a CONNECT tunnel. So -x http://proxy:port https://target is the standard combination, even though the schemes look mismatched.
curl proxy authentication
Paid proxies authenticate with username and password. Two equivalent spellings:
# -U / --proxy-user
curl -x http://gate.example.com:8000 -U username:password https://httpbin.org/ip
# Credentials inline in the proxy URL
curl -x http://username:password@gate.example.com:8000 https://httpbin.org/ip
Prefer -U in scripts. Inline credentials end up in shell history and process lists. Characters like @ or : inside the password also break URL parsing unless percent-encoded. If the proxy rejects the credentials, you get HTTP 407 Proxy Authentication Required. That tells you the proxy is alive and the credentials are the problem. Fixing a 407 through a proxy covers the auth traps in depth.
Chaining two proxies with --preproxy
-x takes one proxy, and --preproxy adds one more in front of it. In the manual's words, curl "first connects to the SOCKS proxy and then connects (through SOCKS) to the HTTP or HTTPS proxy":
# SOCKS5 first (names resolved there), then the HTTP proxy, then the target
curl --preproxy socks5h://198.51.100.14:1080 -x http://203.0.113.7:8080 https://example.com/
That is the limit inside curl: two hops, the first one SOCKS. A longer chain needs a separate tool such as proxychains, with curl pointed at its one local entry.
Environment variables, and the uppercase trap
curl (like most Unix tooling) honors proxy environment variables, which is how you proxy a whole script without touching each command:
export http_proxy="http://203.0.113.7:8080"
export https_proxy="http://203.0.113.7:8080"
export no_proxy="localhost,127.0.0.1"
curl https://httpbin.org/ip # now proxied, no -x needed
The trap: for plain-HTTP requests, curl reads only the lowercase http_proxy. The uppercase HTTP_PROXY is ignored on purpose. CGI servers copy the client's Proxy: request header into HTTP_PROXY, which would let strangers choose your proxy. This class of bug got the name httpoxy when it was disclosed in mid-2016. The CGI standard (RFC 3875) turns an incoming Proxy: header into that variable. PHP, Go, Python, Apache and Tomcat all shipped fixes (CVE-2016-5385 and related). curl's own book says accepting the uppercase form "has been the source for many security problems." The other variables work in both cases. The habit that never fails is lowercase, always.
Two more rules from the same run. The flag beats the variable. With http_proxy pointing at a dead address and --proxy naming a working one, the working proxy got the request. One variable still outranks the flag. curl's manual says NO_PROXY "disables use of the proxy even when specified with the --proxy option". A host listed there goes direct whatever -x says. And on Windows the uppercase rule disappears. Environment names there ignore case, so HTTP_PROXY in capitals was honored on our machine. The lowercase habit still travels best, since it works everywhere.
no_proxy takes a comma-separated list of hosts that bypass the proxy. The one-off flag version is --noproxy "localhost,127.0.0.1". Both took the request off the proxy completely in our run, and the stub proxy logged nothing at all. Per curl's documentation, a leading dot matches a whole domain. Since curl 7.86.0, the list also accepts CIDR ranges like 192.168.0.0/16. When a variable proxies you without your consent, a common CI mystery, curl -v shows the proxy in the connect line. Then --noproxy "*" turns it all off for one command.
Seeing what the target sees
A proxy is only doing its job if the target sees its address, not yours. Verify, never assume:
# Your IP without the proxy
curl -s https://httpbin.org/ip
# Through the proxy: should print the proxy's exit IP
curl -x http://203.0.113.7:8080 -s https://httpbin.org/ip
# What headers arrive at the target (watch for X-Forwarded-For / Via)
curl -x http://203.0.113.7:8080 -s http://httpbin.org/headers
If X-Forwarded-For in that last output contains your real IP, the proxy is transparent grade and hides nothing. Our proxy checker runs the whole battery in one paste: exit IP, anonymity grade, exit location and latency.
Timeouts, timing and retries
Free and overloaded proxies hang more often than they refuse, so give every scripted request a budget:
curl -x http://203.0.113.7:8080 \
--connect-timeout 5 \
--max-time 15 \
--retry 2 --retry-connrefused \
-s https://httpbin.org/ip
--connect-timeout caps the handshake, --max-time caps the whole transfer, and --retry re-attempts transient failures. For measuring a proxy instead of just using it, -w prints timing splits:
curl -x http://203.0.113.7:8080 -o /dev/null -s \
-w "connect: %{time_connect}s ttfb: %{time_starttransfer}s total: %{time_total}s\n" \
https://example.com/
time_connect isolates how far away and how loaded the proxy is. Our own verification engine leans on that number when it grades latency.
Recipes for whole lists
Single proxies are for debugging; real work uses pools. These three recipes cover most of it.
Fetch fresh proxies programmatically. Our free API returns the live pool in plain text, JSON or CSV, no key required:
# 20 fresh HTTP proxies, one ip:port per line
curl -s "https://hproxy.com/api/proxy-list?format=txt&protocol=http&recent=true&limit=20"
Test a whole list in parallel, keep the survivors. Feed any list (that API, or a file) through xargs:
curl -s "https://hproxy.com/api/proxy-list?format=txt&recent=true&limit=100" |
xargs -P 10 -I{} sh -c \
'curl -x http://{} --connect-timeout 5 --max-time 10 -s -o /dev/null \
-w "%{http_code} {}\n" https://httpbin.org/ip' |
grep '^200' | awk '{print $2}' > working.txt
Ten parallel workers run with a five-second connection budget. working.txt ends up holding only proxies that completed a real request end to end. Expect heavy attrition on any free list, as we laid out in our take on free proxies.
Rotate per request. Simplest possible rotation, no tooling:
mapfile -t PROXIES < working.txt
for url in $(cat urls.txt); do
p=${PROXIES[RANDOM % ${#PROXIES[@]}]}
curl -x "http://$p" --max-time 15 -s "$url" -o "out/$(basename "$url").html"
done
For production work, a gateway rotates on the server side: one endpoint, a fresh residential IP when you want one, and no list to manage. The loop above still shows best what rotation actually does.
curl with a residential gateway
A residential gateway is one host and port with home addresses behind it, so the curl side stays a single -x. The login carries your targeting, and our dashboard generates the lines for you. The hosts and ports themselves are public:
| Product | Host | HTTP port | SOCKS5 port |
|---|---|---|---|
| Residential Lite | gw.hproxy.com | 6969. | 9696. |
| Residential Premium | premium.hproxy.com | 9000 rotating, 10000 and up sticky. | 11000 rotating, 12000 and up sticky. |
# Lite over HTTP: country targeting, set in the generated login
curl -x http://gw.hproxy.com:6969 -U "YOUR-LOGIN:YOUR-PASSWORD" https://icanhazip.com
# Premium over SOCKS5, with the gateway resolving names
curl -x socks5h://premium.hproxy.com:11000 -U "YOUR-LOGIN:YOUR-PASSWORD" https://icanhazip.com
Everything earlier on this page applies unchanged. -U keeps the password out of the URL, socks5h keeps DNS lookups off your resolver, and scripts still need timeouts. Lite costs $0.50/GB for one GB, falling to $0.44/GB at 2,000 GB+, and targets countries. Premium costs $1.49/GB for one GB and $1.00/GB at 1,000 GB+. It adds state, city, network (ASN) and device targeting, with sticky sessions from 3 minutes to 24 hours. Both are billed by the GB with no subscription; the residential proxies page has the details.
Common curl proxy errors, by message
| You see | It means | Fix |
|---|---|---|
curl: (7) Failed to connect | Proxy unreachable: dead, wrong port, or firewalled. | Try another proxy. On free lists, most entries are dead at any moment. |
curl: (28) Connection timed out | Proxy accepted TCP then went silent, or is overloaded. | Lower --connect-timeout and move on faster. |
HTTP 407 | Proxy wants credentials, or rejects yours. | Check -U and any special characters that need encoding. |
curl: (35) SSL connect error | TLS handshake broke inside the tunnel. | Often a proxy meddling with TLS. Distrust that proxy. |
curl: (56) Proxy CONNECT aborted | Proxy refused to tunnel to that host or port. | The proxy's policy blocks it, common on ports other than 443. |
| Empty reply / HTML you didn't ask for | Proxy injected an error or ad page. | Free-proxy behavior. Discard it. |
Each of these exit codes gets a fuller decode in our cURL proxy error guide. The rule for all of them: find out whether the failure is on the proxy side or the target side. Swap exactly one variable at a time. Same request, no proxy: does it work? Same proxy, boring target like httpbin: does it work? Two commands, and the mystery is gone.
Where to go from here
Keep a fresh pool within reach: the free proxy list re-verifies its entries every few minutes. Check anonymity with the checker before you trust any proxy with real traffic. When a project moves to production, a gateway billed by the GB replaces the list management, and pricing shows both residential ladders. Every part of an account is reachable over HTTP too. The API documentation gives the curl calls for keys, wallet, orders and plan generation.
Sources
- Everything curl, the proxy environment variables, read September 16, 2026: "It is only accepted in its lower case version because of the CGI protocol", and "All these proxy environment variable names except http_proxy can also be specified in uppercase".
- Everything curl, SOCKS proxies, read September 16, 2026: with
socks5"curl resolves the name", while SOCKS5-hostname "sends the hostname to the proxy so there is no name resolving done by curl locally". - httpoxy, mid 2016, CVE-2016-5385 and others: "the HTTP Proxy header from a request" becomes
HTTP_PROXYin a CGI environment, which is why curl refuses the uppercase form. - RFC 1928, March 1996, for the address type byte that carries either an address or a domain name.
- libcurl error codes, read September 16, 2026, for exit 7, 28 and 97.
- curl user survey analyses by Daniel Stenberg: 2025 (July 4, 2025), 2024 (June 17, 2024), 2023 (June 17, 2023) and 2022 (June 16, 2022), read October 22, 2026: the share of respondents using an HTTP, SOCKS or HTTPS proxy.
- curl manual, curl 8.21.0, read October 22, 2026:
--noproxy,NO_PROXY,--preproxyand-U. - Measured by HProxy on September 16, 2026, with curl 8.16.0: seventeen runs against a stub HTTP proxy, a stub SOCKS5 proxy and a stub target, all on this machine. The proxies recorded the request line, the headers and the SOCKS address type. Nothing outside the machine was contacted.



