Try 500 MB of US mobile proxy data free for 30 days.Start free trial
October 23, 202612 min read

HTTPX Proxy Setup in Python: proxy=, mounts and SOCKS5

Geometric paths passing through proxy gateways

To send HTTPX traffic through a proxy, pass the proxy URL to the client: httpx.Client(proxy="http://user:pass@host:port"). That one argument routes both HTTP and HTTPS requests. Use mounts when different destinations need different proxies, and install the socks extra when your proxy speaks SOCKS5. The rest of this guide covers the details that break scrapers in production: the deprecated proxies= argument, TLS handshake timeouts, environment variables that quietly override your code, and retry logic that separates proxy failures from target failures.

TL;DR

  • Use proxy= on httpx.Client or httpx.AsyncClient for a single proxy. Use mounts= for per-destination routing. The old proxies= argument is deprecated.
  • Write the proxy URL with the http:// scheme even when the target site is HTTPS. HTTPX tunnels through the proxy with CONNECT.
  • For SOCKS, run pip install httpx[socks] and use a socks5:// URL.
  • Set trust_env=False if you want your code, not stray HTTP_PROXY variables, to decide routing. Catch httpx.ProxyError separately from other errors.
  • For geo-targeted or block-prone work, the proxy type matters as much as the client code. We cover that near the end.

Table of Contents

The basic HTTPX proxy setup

The HTTPX proxy documentation shows the pattern: pass the proxy URL on client initialization, or to top-level helpers such as httpx.get(..., proxy=...).

import httpx

PROXY = "http://USERNAME:[email protected]:8080"

with httpx.Client(proxy=PROXY, timeout=20.0) as client:
    r = client.get("https://httpbin.org/ip")
    print(r.json())

The async version is the same shape:

import asyncio, httpx

async def main():
    async with httpx.AsyncClient(proxy=PROXY, timeout=20.0) as client:
        r = await client.get("https://httpbin.org/ip")
        print(r.json())

asyncio.run(main())

Print the IP the target sees on your first run. If it shows your own address, the proxy is not being used. If it shows the proxy's address, you are routed correctly and can move on to the target site.

Replace proxy.example.com:8080 and the credentials with the values from your provider's dashboard. Keep them out of source control: read them from environment variables or a secrets manager.

Why use a Client at all

For anything beyond a one-off script, the HTTPX docs recommend a Client instance, which is the equivalent of requests.Session() in the clients documentation. A client reuses connections, so you pay the proxy handshake once per connection instead of once per request. Using it as a context manager also closes the connection pool cleanly, which matters in high-volume jobs where clients are created and discarded as proxies change.

proxy= versus proxies= (and why old tutorials break)

Many tutorials still show httpx.Client(proxies={...}). That argument was deprecated when the maintainers added proxy in pull request 2879. The reasoning: mounts can already replace proxies for complex routing, so a simple single-proxy argument covers the common case and mounts covers the rest.

In practice:

You wantUse
One proxy for all trafficproxy="http://..."
Different proxies per scheme or domainmounts={...}
No proxy for certain hostsmounts with None for that pattern
Copy-pasted proxies={...} from an old postRewrite to one of the above

If you maintain code that still passes proxies=, migrate it now. Your code may run on a newer release than the tutorial you copied it from, and a removed argument fails at startup.

Routing different traffic through different proxies

The mounts dictionary maps URL patterns to transports. Each transport carries its own proxy.

URL patterns routing requests through separate proxy transports

import httpx

mounts = {
    "https://": httpx.HTTPTransport(proxy="http://USER:[email protected]:8080"),
    "http://": httpx.HTTPTransport(proxy="http://USER:[email protected]:8080"),
    "https://internal.example.com": None,  # bypass the proxy
}

with httpx.Client(mounts=mounts) as client:
    client.get("https://example.com")
    client.get("https://internal.example.com/health")

For async code, use httpx.AsyncHTTPTransport. The more specific pattern wins, so you can send everything through a proxy and carve out exceptions for internal services. The docs note that for a request to a different proxy you should use a different client instance, which is also the cleanest way to run several geographic profiles side by side: one client per target city or region.

Use http:// in the proxy URL, even for HTTPS sites

This is the mistake we see most often. A developer scraping an HTTPS site writes proxy="https://proxy.example.com:8080" and gets a timeout.

HTTPX reaches HTTPS targets through an HTTP proxy by opening a CONNECT tunnel and then upgrading the connection to TLS. The scheme in the proxy URL describes how you talk to the proxy itself, not to the target. Most proxy gateways accept plain HTTP on that first hop. The troubleshooting page lists the symptom: an httpx.ProxyError reporting that the handshake operation timed out on HTTPS requests. The fix is to switch the proxy URL to http://.

Two related points:

  • HTTPX does not natively support a TLS-encrypted connection to the proxy (HTTPS-to-HTTPS proxying). If your provider requires one, it will fail with SSL errors.
  • If jobs fail only on HTTPS targets, check that the proxy supports the CONNECT method and that your credentials are accepted during the CONNECT step. Our guide to HTTP vs HTTPS proxy explains the decision in more depth, and fixing 407 errors with HTTP proxy authentication covers the case where the proxy rejects your credentials.

Authentication

Put credentials in the proxy URL as shown above. If the username or password contains special characters such as @, : or /, percent-encode them first:

from urllib.parse import quote

user = quote("[email protected]", safe="")
pwd = quote("p@ss:word/1", safe="")
PROXY = f"http://{user}:{pwd}@proxy.example.com:8080"

Unencoded characters are the usual reason a correct password still returns 407 Proxy Authentication Required. When you test, take HTTPX out of the picture with a quick curl call. Our curl proxy command cheat sheet gives you commands that separate a credential problem from a client problem.

SOCKS5 proxies in HTTPX

HTTPX supports SOCKS, but not in the core package. The proxy docs say to install the optional extra:

pip install "httpx[socks]"

Then use a socks5:// (or socks4://) URL:

client = httpx.Client(proxy="socks5://USER:[email protected]:1080")

If you see an error about missing SOCKS support, the extra is not installed in the environment that is running the code. This commonly happens when a Docker image or virtualenv differs from your laptop.

A caveat from the community: users have reported that HTTPX has no SOCKS4/4a support in some configurations, so prefer SOCKS5 when you have the choice. Some teams also use the third-party httpx-socks package, which provides sync and async proxy transports, when they need more control than the built-in option.

Whether to pick SOCKS5 or HTTP in the first place is a separate question. It depends on DNS handling and which traffic you need to carry. We break it down in five tests for choosing SOCKS5 or HTTP.

Stop environment variables from overriding your code

By default, HTTPX reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY from the environment, as described in the environment variables documentation. That is convenient on a developer machine and a hazard in production. A CI runner, a container base image or a corporate shell profile can send your scraper through a proxy you did not choose, and nothing in the code shows it.

If your script should be the single source of truth for routing, disable it:

client = httpx.Client(proxy=PROXY, trust_env=False)

The opposite problem exists too. If you expect a system-wide proxy to apply and nothing happens, check whether someone set trust_env=False further up the stack.

Handling proxy failures in your retry logic

A proxy job can fail in three different places, and each needs a different response:

  1. The tunnel never forms. HTTPX raises httpx.ProxyError, an error while establishing the proxy connection. The request never reached the target.
  2. The target is slow or unreachable. You get a timeout or connect error from the transport layer.
  3. The target answers with an error status. You get a 403, 429 or 5xx response. You decide whether to raise with raise_for_status().

Catching ProxyError on its own lets you avoid wasting retries on a gateway that is misconfigured, and lets you rotate or alert instead.

import httpx, time

def fetch(client, url, attempts=3):
    for i in range(attempts):
        try:
            r = client.get(url)
            if r.status_code in (429, 503):
                time.sleep(2 ** i)
                continue
            r.raise_for_status()
            return r
        except httpx.ProxyError:
            raise  # infrastructure problem: do not hammer it
        except (httpx.ConnectTimeout, httpx.ReadTimeout):
            time.sleep(2 ** i)
    raise RuntimeError(f"gave up on {url}")

Whether to re-raise or switch to a fallback on ProxyError depends on your setup. Our write-up on four proxy failover strategies shows how to cap retries so you do not burn bandwidth on requests that were never routed. That matters most on metered plans.

Rotating and sticky sessions from Python

HTTPX does not rotate proxies for you. Rotation happens at one of two layers:

  • At the gateway. You use one proxy URL, and the provider assigns a new exit IP per request or per session.
  • In your code. You keep a list of proxy URLs and create or select a client for each.

Gateway-side rotation is simpler and keeps connection reuse predictable. A sticky session is the opposite need: you keep the same exit IP across several requests, for example when a login, a cart or a paginated flow expects one consistent address. With a gateway that supports both modes, you generally pick the mode through the credentials or the endpoint, as your provider documents.

If you rotate in code, create a client per proxy and close them when done:

import itertools, httpx

proxies = ["http://u:[email protected]:8080", "http://u:[email protected]:8080"]
cycle = itertools.cycle(proxies)

def get(url):
    with httpx.Client(proxy=next(cycle), trust_env=False, timeout=20) as c:
        return c.get(url)

This builds a fresh connection for every call, so for high volume keep a small pool of long-lived clients instead.

Why the proxy type matters as much as the code

A perfectly configured HTTPX client can still get blocked. Many sites score the IP before they look at anything else, and datacenter ranges are well known. If you scrape pages that vary by location, check ads, compare prices across regions or collect training data from sites that guard against automation, the exit IP decides what you see and whether you see anything.

That is the problem mobile proxies address. Traffic exits through IP addresses assigned by mobile carriers, and many real users share those addresses, which makes them harder for a site to treat as a bot by IP alone. It is not a guarantee, and you still need sensible request rates and headers. If you are weighing options, our checklist of eight proxy API features data teams should compare first is a good place to start.

Our perspective

We build the proxy side of this stack, so we see the same HTTPX questions come in from developers. Most are not really about HTTPX. They are about where in the chain a request failed.

Our perspective — overview diagram

Our advice is to debug in layers. First confirm the exit IP with a plain call. Then confirm the target loads from that IP in a browser or with curl. Only then blame the client library. In our experience, a large share of "HTTPX proxy not working" reports come down to an https:// proxy scheme, an unencoded password, a missing SOCKS extra, or an environment variable nobody remembered setting. Each of these is on this page.

We also think sales calls and long contracts are a poor fit for engineering work. You should be able to create credentials, point a client at a gateway, watch usage in a dashboard and change course. That is how we designed MaskLabs: real US mobile carrier IPs, city-level targeting, sticky and rotating sessions, HTTP, HTTPS and SOCKS5 support, and self-serve credential management for individuals and teams.

— Jon

Running HTTPX through MaskLabs mobile IPs

If your project needs authentic mobile IPs in specific US cities, MaskLabs gives you real carrier addresses in 46 cities, with rotating or sticky sessions, API access and per-byte metered billing. You can start with a free trial that includes limited data, then pick a monthly plan on the pricing page: Starter at $30.00 per month, Basic at $100.00 per month, Advanced at $500.00 per month, or Scale at $1,000.00 per month. Prepaid data add-ons are billed per GB if you need more than your plan includes.

The HTTPX setup is the one above: copy the host, port and credentials from your dashboard into the proxy= argument, keep the http:// scheme, and use trust_env=False so nothing overrides it. If you also work in other languages, we publish matching guides for Node.js with Axios, Java and Apache HttpClient, C# HttpClient and Rust.

A production checklist

Before you ship an HTTPX scraper, run through this list:

  1. The proxy URL uses http:// (or socks5:// with the extra installed).
  2. Credentials with special characters are percent-encoded and stored outside the code.
  3. trust_env=False is set, unless you deliberately want environment routing.
  4. You use a long-lived Client or AsyncClient as a context manager.
  5. Timeouts are explicit. Do not rely on defaults for a job that touches thousands of URLs.
  6. httpx.ProxyError is handled apart from timeouts and HTTP status errors.
  7. You log the exit IP on startup, so a routing mistake shows in the first line of output.
  8. You use sticky sessions only where a flow needs one IP, and rotating sessions everywhere else.

FAQ

How do I set a proxy in HTTPX?

Pass the URL to the client: httpx.Client(proxy="http://user:pass@host:port"), or to a helper such as httpx.get(url, proxy=...). The same argument works on AsyncClient. Use mounts only when you need different proxies for different destinations.

Why does HTTPX say the handshake operation timed out with my proxy?

The usual cause is an https:// scheme in the proxy URL. Most proxy gateways expect plain HTTP on the first hop and then open a CONNECT tunnel to the HTTPS target. Change the proxy URL to http:// and retry.

Does HTTPX support SOCKS5 proxies?

Yes, after you install the extra with pip install "httpx[socks]". Then use a socks5:// URL as the proxy value. If the package is missing in your runtime, HTTPX raises an error when it tries to use the SOCKS transport.

What happened to the proxies= argument?

It was deprecated in favor of proxy= for a single proxy and mounts= for routing rules. Old tutorials still use it, so update any code that does.

Is my script using a proxy I never configured?

Possibly. HTTPX reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY by default. Set trust_env=False on the client to ignore them.

Sources

Recommended