Skip to content
Web scraping

How to use a proxy with curl

Every curl proxy flag that matters, the socks5h trap, how to verify your exit address, and how to read the errors.

curl is the fastest way to establish whether a proxy works at all, which is why it is the first thing to reach for when something breaks. This guide covers the flags that matter, the one trap that catches nearly everyone, and how to read the failures.

#The basic form

# HTTP proxy, no authentication
curl -x http://gateway.example:8000 https://api.example.com/v1/items

# With credentials inline
curl -x http://user:[email protected]:8000 https://api.example.com/v1/items

# Credentials in a separate flag, which keeps them out of the URL
curl -x http://gateway.example:8000 -U user:pass https://api.example.com/v1/items

-x is simply the short form of --proxy; they are identical. Separating credentials with -U is worth the habit, because a URL containing a password ends up in your shell history.

#Choosing the scheme

Scheme What it does Use when
http:// HTTP proxy; HTTPS travels via a CONNECT tunnel The default for most providers
https:// The connection to the proxy itself is TLS-encrypted The provider explicitly supports it
socks5:// SOCKS5, hostname resolved locally Rarely what you want
socks5h:// SOCKS5, hostname resolved at the proxy Almost always the right SOCKS choice

#The socks5h trap

This is the single most common configuration mistake, and it fails silently rather than loudly.

With socks5://, your machine performs the DNS lookup and sends a resolved IP address to the proxy. Two consequences follow. First, your DNS queries leak to whatever resolver you use locally. Second — and more damaging for geo-targeted work — you resolve the hostname from your location. Large sites return different addresses per region, so you can end up fetching the wrong regional endpoint while your proxy is dutifully connecting from the correct country.

# Leaks DNS, resolves locally — usually wrong
curl -x socks5://gateway.example:1080 https://example.com

# Resolves at the proxy — usually right
curl -x socks5h://gateway.example:1080 https://example.com

Unless you have a specific reason to resolve locally, use socks5h.

#Verifying the exit address

Never assume the proxy is doing what you configured. Ask something to echo the address back:

curl -x http://user:[email protected]:8000 \
     -s https://api.ipify.org?format=json

Compare that with your address without the proxy. If they match, the proxy is not being applied — check for an http_proxy environment variable overriding you, or a typo in the scheme.

Remember that on a rotating endpoint this reports one exit node at one moment, not a property of the pool.

#Measuring where the time goes

“The proxy is slow” is rarely actionable. Split the timing:

curl -x http://gateway.example:8000 -o /dev/null -s \
  -w "connect: %{time_connect}s\nappconnect: %{time_appconnect}s\ntotal: %{time_total}s\ncode: %{http_code}\n" \
  https://api.example.com/v1/items

A high time_connect points at the path to the proxy or the proxy’s own queueing. A high time_appconnect is the TLS handshake through the tunnel. A high time_total with both of the others low means the destination is slow, and the proxy is not your problem.

#Reading the errors

Exit code Meaning Where to look
7 Could not connect to the proxy Host, port, firewall, or the proxy is down
28 Timeout Raise --connect-timeout; check concurrency limits
35 TLS handshake failure Scheme mismatch, or interception on the path
56 Failure receiving data Tunnel dropped mid-transfer, often an exit rotating away

HTTP status codes matter just as much. A 407 is your proxy rejecting your credentials. A 401 is the destination rejecting you, which means the proxy worked. A 429 is rate limiting and means you are going too fast, not that your proxy is bad.

#Practical flags worth knowing

# Bypass the proxy for specific hosts
curl -x http://gateway.example:8000 --noproxy "localhost,127.0.0.1,.internal" https://api.example.com

# Send several requests over one connection
curl -x http://gateway.example:8000 https://example.com/a https://example.com/b

# Show the CONNECT exchange and response headers
curl -x http://gateway.example:8000 -v https://api.example.com 2>&1 | head -30

The verbose output is where you confirm the tunnel was actually established — look for Connection established before the TLS lines.

#A note on environment variables

curl honours http_proxy, https_proxy and no_proxy. These are a frequent source of confusion because they apply invisibly. If a request behaves unexpectedly, check them first:

env | grep -i proxy

An explicit -x overrides them, which makes -x the safer choice in scripts you want to be reproducible.

#Testing a list of proxies

A short loop is usually enough to separate working endpoints from dead ones. This reports the exit address and the timing for each, and marks failures clearly:

#!/bin/bash
# proxies.txt: one per line, host:port:user:pass
while IFS=: read -r host port user pass; do
  out=$(curl -sS --max-time 15 \
        -x "http://$user:$pass@$host:$port" \
        -w '%{http_code} %{time_total}' \
        -o /tmp/body https://api.ipify.org 2>/dev/null)

  if [ $? -ne 0 ]; then
    printf '%-28s FAILED\n' "$host:$port"
  else
    printf '%-28s %s exit=%s\n' "$host:$port" "$out" "$(cat /tmp/body)"
  fi
done < proxies.txt

Two details make this more useful than it looks. --max-time stops one dead endpoint stalling the whole run. And printing the exit address rather than only the status confirms the proxy actually applied — a 200 that returns your own address means it did not.

#Checking geography properly

Providers advertise a country for the exit node, not for the gateway you dial. Verify the exit:

curl -x "http://user-country-de:[email protected]:8000" -sS \
  "http://ip-api.com/json/?fields=status,country,city,isp,as"

Expect disagreement between geolocation sources at city level; geo-targeting is inference, not fact. Country-level results are generally reliable. If the country is wrong, the usual causes are a malformed parameter in the username, or a pool with no availability in that country at that moment — in which case some providers silently fall back to another location rather than failing.

#Sessions from the command line

To hold one address across several requests, most providers accept a session identifier in the username:

SESSION="s-$(openssl rand -hex 4)"
PROXY="http://user-session-$SESSION:[email protected]:8000"

curl -x "$PROXY" -sS https://example.com/step-one -c jar.txt
curl -x "$PROXY" -sS https://example.com/step-two -b jar.txt

The cookie jar matters as much as the session. Holding the address while discarding cookies still breaks the flow, because the server tracks both. See rotating vs sticky sessions for when this is required.

#Making curl look less like curl

By default curl announces itself. For diagnostics that is fine and often preferable. If you are reproducing what a browser sees, you need more than a User-Agent:

curl -x http://gateway.example:8000 \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-GB,en;q=0.9' \
  -H 'Accept-Encoding: gzip, deflate, br' \
  --compressed \
  https://target.example/page

Be clear about what this does and does not achieve. It fixes header content. It does not change curl's TLS fingerprint, which arrives before any header and identifies the client library regardless. Against a target that fingerprints TLS, these headers create an inconsistency rather than a disguise — a browser User-Agent on a curl handshake is a clearer automation signal than leaving the default alone.

#Bandwidth, if you are billed for it

On a per-gigabyte plan every byte counts, including headers and retries. Two flags help materially:

# Request compression and actually decompress it
curl -x http://gateway.example:8000 --compressed https://target.example/page

# Fetch headers only, when you just need to check status or a redirect
curl -x http://gateway.example:8000 -I https://target.example/page

-I issues a HEAD request, which many endpoints answer with headers and no body at all. For availability checks that is a large saving over fetching the page.

#A checklist when a proxy "does not work"

  1. Does env | grep -i proxy show something overriding you?
  2. Does an echo service return the proxy's address, or your own?
  3. Is the status 407? Then it is credentials, not the target.
  4. Does -v show Connection established?
  5. Is the exit country what you asked for?
  6. Does the same request succeed without the proxy? If not, the target is the problem.

Working down that list in order resolves the large majority of cases, and it stops you rewriting application code when the actual fault is one environment variable.

#Reproducing a browser request exactly

When a page works in your browser and fails in curl, the fastest route to the answer is to stop guessing and copy the real request. Every major browser will do this for you: open developer tools, go to the Network tab, right-click the request and choose Copy as cURL.

You get a command containing every header the browser actually sent, in the order it sent them. Add your proxy flag and run it:

# Paste the copied command, then add -x before the URL
curl -x http://user:[email protected]:8000 \
  'https://target.example/api/items' \
  -H 'accept: application/json' \
  -H 'accept-language: en-GB,en;q=0.9' \
  -H 'referer: https://target.example/' \
  --compressed

If that succeeds and your original failed, bisect: remove headers one at a time until it breaks again. The header you removed last is the one that mattered. It is frequently Referer, Accept-Language, or an application-specific token the page sets in JavaScript.

If the copied command also fails through the proxy while working without it, the address is the variable and you have your answer from the other direction.

#When curl succeeds and your code does not

This happens often enough to be worth naming. curl and your HTTP library differ in ways that are invisible in a log:

  • Header order and casing. Libraries normalise; browsers and curl do not necessarily.
  • TLS fingerprint. curl, Python and Go all handshake differently, and the destination sees that before any header.
  • HTTP version. curl may negotiate HTTP/2 where your library defaults to 1.1, or the reverse.
  • Compression. --compressed both requests and decompresses; some libraries request without handling it.

Force curl down to the same settings your library uses, and see whether it starts failing too. That converts an unreproducible bug into a controlled comparison.

Frequently asked questions

What is the difference between -x and --proxy in curl?
Nothing. -x is the short form of --proxy and they behave identically. Both accept a scheme, host and port, and optionally inline credentials.
Why does curl work but my browser does not?
Most often DNS. With socks5 your machine resolves the hostname; with socks5h the proxy does. If the target serves different content per region, resolving locally can send you to the wrong endpoint even though the proxy is in the right country.
How do I stop curl leaking my real IP through DNS?
Use socks5h rather than socks5 for SOCKS proxies. For HTTP proxies, HTTPS requests use CONNECT with the hostname, so the proxy resolves it and there is no local DNS lookup for the destination.
What does curl exit code 7 mean with a proxy?
Failed to connect to the proxy itself. The host or port is wrong, the proxy is down, or a firewall is blocking the outbound connection. It is not a problem with the destination, because curl never got that far.

Sources

  1. curl manual page: proxy options
  2. RFC 1928: SOCKS Protocol Version 5

Read this page as Markdown · Quote it freely under CC BY 4.0 with a link back.