How to Use cURL With a Proxy: -x, Auth, Env Variables, no_proxy

Rewritten as a tested reference. Every command below was run against local proxies started by the example package (an HTTP proxy with Basic authentication, a SOCKS5 proxy with authentication, an open HTTP proxy) and the output is copied from that run. The previous version repeated its configuration section three times, used placeholder hosts throughout, and contained a mangled example address; all of that is gone.
The short answer:
curl -x http://proxy.example.com:8080 https://example.com/ # through an HTTP proxy
curl -x http://proxy.example.com:8080 --proxy-user user:pass https://example.com/ # with authentication
curl -x socks5h://user:pass@proxy.example.com:1080 https://example.com/ # SOCKS5, proxy resolves names
-x (long form --proxy) takes [scheme://][user:pass@]host[:port]; without a scheme curl assumes an HTTP proxy, and without a port it assumes 1080. The rest of this page shows what actually happens on the wire for each variant, how to set a proxy through the environment or .curlrc, how to switch it off for some hosts, and how the common failures look. Transcripts marked -v are trimmed to the proxy-related lines (a real -v prints a lot more), and non-local IP addresses are masked as <ip>. Tested with curl 8.7.1.
The simplest real run, through an open HTTP proxy on localhost:
curl -x http://127.0.0.1:8081 -o /dev/null -w '%{http_code}\n' https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
200
exit=0
Three ways a request can go through a proxy
What the proxy sees depends on the target scheme and the proxy type. This decides how errors surface and what a proxy can log.
For an http:// target, curl sends the whole request to the proxy, including the full URL and any Proxy-Authorization header, and the proxy forwards it:
curl -v -x http://127.0.0.1:8080 --proxy-user user:pass -o /dev/null http://127.0.0.1:8000/index.html
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET http://127.0.0.1:8000/index.html HTTP/1.1
> Host: 127.0.0.1:8000
> Proxy-Authorization: Basic dXNlcjpwYXNz
< HTTP/1.0 200 OK
For an https:// target, curl asks the proxy to open a tunnel with CONNECT and then speaks TLS to the site through it. The proxy sees the CONNECT line with the hostname and port, the headers attached to it (including Proxy-Authorization), and after that only encrypted bytes; it does not see the path, the request headers or the body:
curl -v -x http://127.0.0.1:8080 --proxy-user user:pass -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 8080
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
> Proxy-Authorization: Basic dXNlcjpwYXNz
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
This is why "an HTTP proxy" still works for HTTPS sites, and why adding -k when a proxy "has SSL problems" does not help: the manual defines -k (--insecure) as skipping verification of the server, and has separate --proxy-insecure and --proxy-cacert options for the proxy's own certificate.
Authentication
Two equivalent forms. --proxy-user (short -U) keeps the credentials out of the URL:
curl -x http://127.0.0.1:8080 --proxy-user user:pass -o /dev/null -w '%{http_code}\n' https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
200
exit=0
Or embed them in the proxy string; curl URL-decodes them, so an @ in a password is written %40:
curl -x http://user:pass@127.0.0.1:8080 -o /dev/null -w '%{http_code}\n' https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
200
exit=0
Basic is curl's default proxy authentication. The manual warns that a password on the command line is briefly visible to other users of the machine; for anything shared, keep it in a file such as .curlrc (below) instead.
When it fails: 407, exit 56, exit 7
Wrong or missing credentials produce 407 Proxy Authentication Required, but how curl reports it depends on the target scheme. For an https:// target the 407 answers the CONNECT, the tunnel never opens, and curl exits with code 56:
curl -x http://127.0.0.1:8080 -o /dev/null -w '%{http_code}\n' https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
000
exit=56
For an http:// target the 407 is simply the response, and curl exits 0 with status 407:
curl -x http://127.0.0.1:8080 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/index.html
407
exit=0
-v shows the exchange, including the Proxy-Authenticate header that tells you which scheme the proxy wants:
curl -v -x http://127.0.0.1:8080 --proxy-user user:wrong -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 8080
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
> Proxy-Authorization: Basic dXNlcjp3cm9uZw==
< HTTP/1.1 407 Proxy Authentication Required
< Proxy-Authenticate: Basic
* CONNECT tunnel failed, response 407
A proxy that is not listening on the given port is exit code 7:
curl -x http://127.0.0.1:9999 -o /dev/null -w '%{http_code}\n' https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
000
exit=7
A proxy hostname that does not resolve is exit code 5, before any connection is attempted:
curl -x http://proxy.invalid:8080 -o /dev/null -w '%{http_code}\n' https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
000
exit=5
If the proxy itself speaks TLS (-x https://…), curl verifies the proxy's certificate the same way it verifies a site's. Use --proxy-cacert file.pem to trust a private CA for the proxy, and --proxy-insecure only for a throwaway test. Those two options are documented in the curl manual; the example package has no TLS proxy, so they were not run here.
SOCKS5: socks5:// versus socks5h://
With socks5://, curl resolves the hostname itself and sends the IP to the proxy. With socks5h://, curl sends the hostname and the proxy resolves it. The difference shows up in -v:
curl -v -x socks5://user:pass@127.0.0.1:1080 -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 1080
* Host scrapingant.github.io:443 was resolved.
* SOCKS5 connect to <ip>:443 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
curl -v -x socks5h://user:pass@127.0.0.1:1080 -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 1080
* SOCKS5 connect to scrapingant.github.io:443 (remotely resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
Use socks5h whenever the proxy's view of DNS is the point: SSH -D tunnels, Tor, or a proxy in another country whose resolver returns region-specific addresses. Authentication for SOCKS5 goes in the proxy string or --proxy-user, as above.
Setting the proxy through the environment
curl reads http_proxy, https_proxy (or HTTPS_PROXY), ALL_PROXY and no_proxy (or NO_PROXY). Setting one has the same effect as -x. Only one rule is easy to get wrong: http_proxy is read in lowercase only. The reason, per everything.curl.dev, is that the CGI convention turns a request header into an uppercase HTTP_PROXY variable, which made honouring it a source of security problems. The run shows the difference:
http_proxy=http://127.0.0.1:8081 curl -v -o /dev/null http://127.0.0.1:8000/index.html
* Uses proxy env variable http_proxy == 'http://127.0.0.1:8081'
* Connected to 127.0.0.1 (127.0.0.1) port 8081
> GET http://127.0.0.1:8000/index.html HTTP/1.1
> Host: 127.0.0.1:8000
< HTTP/1.0 200 OK
HTTP_PROXY=http://127.0.0.1:8081 curl -v -o /dev/null http://127.0.0.1:8000/index.html
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> GET /index.html HTTP/1.1
> Host: 127.0.0.1:8000
< HTTP/1.0 200 OK
Uppercase is fine for the others:
https_proxy=http://127.0.0.1:8081 curl -v -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Uses proxy env variable https_proxy == 'http://127.0.0.1:8081'
* Connected to 127.0.0.1 (127.0.0.1) port 8081
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
ALL_PROXY covers every protocol that has no specific variable of its own:
ALL_PROXY=http://127.0.0.1:8081 curl -v -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Uses proxy env variable ALL_PROXY == 'http://127.0.0.1:8081'
* Connected to 127.0.0.1 (127.0.0.1) port 8081
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
Bypassing the proxy: no_proxy, --noproxy, -x ''
Who wins: an explicit -x beats the environment and .curlrc; the no-proxy list beats everything for the hosts it names.
no_proxy is a comma-separated list of hosts and domains that must not go through the proxy. Each name matches the host itself and any host whose domain contains it; * alone matches everything; and since curl 7.86.0 IP ranges can be given in CIDR notation such as 192.168.0.0/16 (curl manual, ENVIRONMENT). With the variable set, curl connects directly:
https_proxy=http://127.0.0.1:8081 no_proxy=scrapingant.github.io curl -v -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Uses proxy env variable no_proxy == 'scrapingant.github.io'
* Host scrapingant.github.io:443 was resolved.
* Connected to scrapingant.github.io (<ip>) port 443
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
The leading-dot form described on everything.curl.dev works as well:
https_proxy=http://127.0.0.1:8081 no_proxy=.github.io curl -v -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Uses proxy env variable no_proxy == '.github.io'
* Host scrapingant.github.io:443 was resolved.
* Connected to scrapingant.github.io (<ip>) port 443
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
--noproxy is the same list given on the command line; the manual notes it overrides no_proxy and NO_PROXY from the environment (since 7.53.0). With '*' it disables the proxy for this one call, and it beats an explicit -x too:
curl -v -x http://127.0.0.1:8081 --noproxy '*' -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Host scrapingant.github.io:443 was resolved.
* Connected to scrapingant.github.io (<ip>) port 443
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
The same flag against an environment proxy:
https_proxy=http://127.0.0.1:8081 curl -v --noproxy '*' -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Host scrapingant.github.io:443 was resolved.
* Connected to scrapingant.github.io (<ip>) port 443
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
An empty -x '' cancels a proxy inherited from the environment (it is itself a -x, so it does not undo a later -x on the same command line):
https_proxy=http://127.0.0.1:8081 curl -v -x '' -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Host scrapingant.github.io:443 was resolved.
* Connected to scrapingant.github.io (<ip>) port 443
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
And an explicit -x beats a conflicting environment variable, which is what you want in scripts:
https_proxy=http://127.0.0.1:9999 curl -v -x http://127.0.0.1:8081 -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 8081
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
Making it permanent: .curlrc
curl reads ~/.curlrc at startup and treats each line as a command-line option. Long option names go without dashes; a value that contains whitespace or starts with : or = must be double-quoted (quoting other values is harmless). A file that makes every curl call use an authenticated proxy:
proxy = "http://127.0.0.1:8080"
proxy-user = "user:pass"
The run used CURL_HOME to point curl at a directory containing that file instead of touching the real home directory; the effect is identical:
CURL_HOME=./curlhome curl -v -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 8080
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
> Proxy-Authorization: Basic dXNlcjpwYXNz
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
An explicit -x on the command line beats the file's proxy line, while the file's proxy-user still applies, which is convenient for a provider account:
CURL_HOME=./curlhome curl -v -x http://127.0.0.1:8081 -o /dev/null https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html
* Connected to 127.0.0.1 (127.0.0.1) port 8081
* CONNECT tunnel: HTTP/1.1 negotiated
> CONNECT scrapingant.github.io:443 HTTP/1.1
> Host: scrapingant.github.io:443
> Proxy-Authorization: Basic dXNlcjpwYXNz
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
> GET /scrapingant-examples/fixtures/dynamic-delayed.html HTTP/2
> Host: scrapingant.github.io
< HTTP/2 200
Keep the file readable only by you: it holds a password.
Using a proxy provider
Everything above works unchanged with a hosted rotating proxy; what changes is where the credentials come from and what the username carries. ScrapingAnt's residential proxies are reached at residential.scrapingant.com (port 8080 for HTTP, 443 for HTTPS with CONNECT) with Basic authentication, and the datacenter pool at datacenter.scrapingant.com on the same ports; the username and password are shown on the account's residential and datacenter settings pages. Targeting is encoded in the username: customer-USERNAME-country-us and -sessionid-<id> for a sticky session on both pools (residential sessions last up to 10 minutes); -state-us_california and -city-us_los_angeles on the residential pool, where city overrides state and state overrides country. Check each pool's own country table, since the codes differ (the datacenter list writes the United Kingdom as UK). The docs' own test command, which returns the exit IP:
curl --proxytunnel --proxy "https://residential.scrapingant.com" --proxy-user "customer-USERNAME-country-us:PASSWORD" https://scrapingbin.link/ip
This one was not run for this article (it needs an account); everything else on the page was. Setup pages: residential, datacenter.
You do not need a provider for a corporate proxy, an SSH -D SOCKS tunnel, or a one-off request from your own address. A provider matters when the target blocks your address or you need to appear in another country.
Quick reference
| Task | Command |
|---|---|
| HTTP proxy | curl -x http://host:8080 URL |
| With credentials | curl -x http://host:8080 --proxy-user user:pass URL |
| SOCKS5, proxy resolves DNS | curl -x socks5h://user:pass@host:1080 URL |
| From the environment | export https_proxy=http://host:8080 |
| Skip the proxy for some hosts | export no_proxy=localhost,internal.example |
| Skip it for one call | curl --noproxy '*' URL or curl -x '' URL |
| See what happened | add -v; look for CONNECT, Proxy-Authorization, Uses proxy env variable |
| Permanent | ~/.curlrc with proxy = … and proxy-user = … |
Limitations
- The proxies in the run are local (
127.0.0.1), so the target sees the same source address either way; the transcripts prove the routing, not an IP change. The provider command that would show a different exit IP was not executed. - No TLS-speaking proxy was set up, so
--proxy-cacertand--proxy-insecureare described from the manual only. - Behaviour was recorded with
curl 8.7.1on macOS; the exit codes and-vlines are curl's, not the platform's, but older versions differ (CIDR inno_proxyneeds 7.86.0;https://proxies need 7.52.0 or, for some TLS backends, 7.87.0).
Related: cURL cheat sheet, ignoring SSL certificate errors with cURL, wget with a proxy, residential vs datacenter proxies.
Examples tested on 2026-09-15 with curl 8.7.1, proxy.py 2.4.10 and pproxy 2.7.9. Code, outputs and diagram sources: scrapingant-examples/examples/curl-with-proxy.
This article was drafted with AI assistance from a tested evidence packet and reviewed by the named author, who is responsible for the code, measurements and corrections.