Requests vs HTTPX vs aiohttp: Proxy Support
Python has three mainstream HTTP clients, and they handle proxies differently: different parameters, different SOCKS stories, different environment-variable defaults. Here is how each one routes traffic through a proxy, and which one fits your scraper.
Quick Answer
All three clients speak to HTTP proxies out of the box; the differences are in concurrency model and SOCKS. requests is synchronous and uses a proxies= dict keyed by scheme. HTTPX offers both sync and async clients, takes a single proxy= argument, and optionally speaks HTTP/2. aiohttp is async-only, takes proxy= per request or per session, and needs the aiohttp-socks package for SOCKS.
- →Small sync scripts: requests. Migrating to async without rewriting everything: HTTPX. Raw async throughput: aiohttp
- →SOCKS5 is an extra install in all three: requests[socks], httpx[socks], or aiohttp-socks
- →requests and HTTPX read HTTP_PROXY/HTTPS_PROXY by default; aiohttp does not unless you pass trust_env=True
If you are building a scraper on dedicated 4G/5G proxies, the HTTP client is the layer that actually carries your credentials, decides where DNS gets resolved, and controls how many connections hit the proxy at once. Picking the wrong one will not break anything, but it will shape how much code you write and how your concurrency behaves. This guide compares the three libraries strictly on proxy handling, with the configuration patterns pulled from their official docs. For the wider picture of running a scraping stack on mobile IPs, see our web scraping solutions.
Proxy support at a glance
| Feature | requests | HTTPX | aiohttp |
|---|---|---|---|
| Concurrency model | Sync only (blocking IO) | Sync Client + AsyncClient | Async only (asyncio) |
| Proxy parameter | proxies= dict keyed by scheme | proxy= URL, or mounts= per scheme | proxy= per request or per session |
| SOCKS5 | requests[socks] extra (PySocks) | httpx[socks] extra | Third-party aiohttp-socks |
| Remote DNS via proxy | socks5h:// scheme | socks5h accepted since 0.28 | rdns=True (default for SOCKS5) |
| Env vars (HTTP_PROXY etc.) | Read by default | Read by default (trust_env=False to ignore) | Ignored unless trust_env=True |
| HTTP/2 | No | Optional, httpx[http2] + http2=True | No (client is HTTP/1.1) |
requests: sync and simple
requests is the default choice for anything that does not need concurrency. Proxies are a dict keyed by URL scheme, passed per request or set on a Session. The docs are explicit that requests "does not provide any kind of non-blocking IO", so scaling beyond a handful of parallel fetches means threads or a different library.
import requests
# HTTP proxy port for the dedicated slot us-mob-01
PROXY = "http://u_4a9c:p_2X7q@proxy.mobileproxies.org:8000"
proxies = {"http": PROXY, "https": PROXY}
r = requests.get("https://api.ipify.org", proxies=proxies, timeout=20)
print(r.text) # carrier IP, not your machine'sSOCKS support arrived in requests 2.10.0 and is an optional extra: pip install "requests[socks]", which pulls in PySocks. The scheme controls DNS: socks5:// resolves hostnames on your machine, socks5h:// resolves them on the proxy, which is what you usually want with a remote mobile proxy. We cover the full pattern, including auth, retries and rotation, in our Python requests SOCKS5 proxy example.
Gotcha: the requests docs warn that values set via session.proxies can be overwritten by environmental proxies (the ones urllib.request.getproxies returns). To guarantee your proxy is used when HTTP_PROXY-style variables exist, pass proxies= on each request.
HTTPX: sync + async, one API
HTTPX gives you a requests-like API in both flavors: a synchronous Client and an AsyncClient. Proxying is configured with a single proxy= argument on the client (or on top-level calls like httpx.get). Note the naming: the older proxies= argument was deprecated in HTTPX 0.26 and removed in 0.28, so snippets written for requests need that one change.
import asyncio
import httpx
PROXY = "http://u_4a9c:p_2X7q@proxy.mobileproxies.org:8000"
# Sync client
with httpx.Client(proxy=PROXY, timeout=20) as client:
print(client.get("https://api.ipify.org").text)
# Async client - same API, awaited
async def main():
async with httpx.AsyncClient(proxy=PROXY, timeout=20) as client:
r = await client.get("https://api.ipify.org")
print(r.text)
asyncio.run(main())For SOCKS, install the extra with pip install httpx[socks] and pass a socks5:// URL, for example httpx.Client(proxy="socks5://u_4a9c:p_2X7q@proxy.mobileproxies.org:1080"). Since 0.28 HTTPX also treats socks5h as a valid scheme for proxy-side DNS resolution. If you need different proxies for different schemes or hosts, the mounts= dict maps URL patterns to httpx.HTTPTransport(proxy=...) instances.
HTTPX is also the only one of the three with optional HTTP/2: install httpx[http2] and pass http2=True. It is off by default, and it only kicks in when the server supports it, but for scraping targets that fingerprint clients at the protocol level, having the option matters.
aiohttp: async-first, SOCKS via a plugin
aiohttp is built for asyncio from the ground up; there is no synchronous mode. HTTP proxies are supported natively: pass proxy= to an individual request or to the ClientSession itself, with credentials embeddable in the proxy URL. The docs note that support for proxies you connect to over https:// is limited; a plain http:// proxy endpoint that tunnels HTTPS traffic via CONNECT, which is how most mobile proxy gateways work, is the supported path.
import asyncio
import aiohttp
PROXY = "http://u_4a9c:p_2X7q@proxy.mobileproxies.org:8000"
async def main():
# Session-level proxy; can also pass proxy= per request
async with aiohttp.ClientSession(proxy=PROXY) as session:
async with session.get("https://api.ipify.org") as resp:
print(await resp.text())
asyncio.run(main())SOCKS is not in aiohttp itself. The standard answer is the third-party aiohttp-socks package, which provides a ProxyConnector supporting SOCKS4(a), SOCKS5(h) and HTTP proxies, with remote DNS (rdns) defaulting to on for SOCKS5.
# pip install aiohttp-socks
import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector
async def main():
connector = ProxyConnector.from_url(
"socks5://u_4a9c:p_2X7q@proxy.mobileproxies.org:1080"
)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get("https://api.ipify.org") as resp:
print(await resp.text())
asyncio.run(main())Whether SOCKS5 is even worth the extra dependency depends on your use case; for most scraping jobs the HTTP CONNECT port behaves identically for HTTPS targets. We break down the real differences in SOCKS5 vs HTTP proxies.
The env-var trap
The single most common source of "why is my proxy not being used" (or "why is a proxy being used") bugs is that the three libraries treat HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY differently.
requests: on by default
Falls back to the standard environment variables when no proxies are passed, and env proxies can even override session-level settings. Set session.trust_env = False or pass explicit proxies= per request to stay in control.
HTTPX: on by default
Uses environment variables by default; pass trust_env=False to the client to ignore them.
aiohttp: off by default
The docs state that, contrary to requests, aiohttp will not read proxy environment variables by default; you opt in with trust_env=True on the session, which also picks up proxy credentials from ~/.netrc.
Which one for a mobile proxy scraper?
An honest note first: with a dedicated mobile proxy, the HTTP client is rarely your bottleneck. Each slot is a real SIM in a real modem, so its bandwidth and sensible request rate are bounded by the carrier connection, not by how fast Python can open sockets. Mobile IPs earn their keep on trust (they sit behind carrier-grade NAT shared with real subscribers), not on raw throughput, and they cost more than datacenter IPs. Plan concurrency around the proxy, then pick the client that matches that plan:
Pick requests if
You run sequential or lightly threaded jobs: checkout flows, account tasks, small crawls. Least code, most Stack Overflow answers, battle-tested proxy behavior.
Pick HTTPX if
You want one API for sync scripts and async workers, may need HTTP/2, or are migrating a requests codebase toward async gradually.
Pick aiohttp if
Your stack is already asyncio-native and you run many concurrent fetches across several proxy slots. Accept the aiohttp-socks dependency if you need SOCKS5.
One more architectural choice interacts with the client: whether your IP rotates. A dedicated slot with API-triggered rotation pairs naturally with a long-lived session object in any of the three libraries, while forced rotation changes how you think about connection reuse. See rotating vs static mobile proxies for how to choose.
Sources
- • requests docs - Proxies and SOCKS (proxies= dict, requests[socks], socks5 vs socks5h, env vars)
- • HTTPX docs - Proxies (proxy=, mounts=, httpx[socks])
- • HTTPX docs - HTTP/2 (httpx[http2], http2=True, off by default)
- • HTTPX docs - Environment variables (trust_env behavior)
- • HTTPX changelog (proxies= deprecated in 0.26, removed in 0.28; socks5h accepted in 0.28)
- • aiohttp docs - Client advanced usage (proxy=, trust_env, HTTPS proxy limits)
- • aiohttp-socks on PyPI (ProxyConnector, SOCKS4/5, rdns)
Related Guides
Developer Guide
HTTPX Proxy Setup in Python
proxy=, mounts, async, and SOCKS5
Developer Guide
aiohttp Proxy Setup
Async proxying with ProxyConnector
Tutorial
Python requests SOCKS5 Proxy Example
Auth, DNS, retries and rotation patterns
Fundamentals
SOCKS5 vs HTTP Proxies
When the protocol choice actually matters
Fundamentals
Rotating vs Static Mobile Proxies
Session strategy for scrapers and accounts
One endpoint, any client
Our dedicated 4G/5G mobile proxies expose both an HTTP port and a SOCKS5 port on the same slot, so the same credentials work in requests, HTTPX and aiohttp. Real carrier SIMs in the US, UK, France and the Netherlands.