Integration · HTTP clients
How to use a proxy with aiohttp
aiohttp takes a proxy per request, session.get(url, proxy="http://user:pass@host:port"), or as a default on the ClientSession. Credentials go in the URL; the separate proxy_auth argument is deprecated since aiohttp 3.14. SOCKS5 is not built in and comes from the aiohttp-socks connector.
Reviewed 30 Oct 2026
http://USERNAME _pool-arcfast _country-de:PASSWORD@ proxy.arcproxies.io:8000The short answer
- Per-request
proxy=makes a sticky handle per task easy: one session, a different proxy URL per task. - aiohttp does not read
HTTP_PROXYand friends unless you passtrust_env=True. - A rejected tunnel raises
ClientHttpProxyErrorwith.status == 407, and its message contains the proxy URL, password included. - SOCKS5 goes through
aiohttp_socks.ProxyConnector, which sets one proxy per session.
Setup
Set up aiohttp with a proxy, step by step
- 01
Install aiohttp
Add
aiohttp-socksonly if you need SOCKS5.shell2 linespip install aiohttppip install aiohttp-socks # optional, for SOCKS5 - 02
Request through the gateway and check the exit
Pass the proxy URL with credentials to the request. The echo service returns the exit IP the target sees.
check_exit.py13 linesimport asyncioimport aiohttpPROXY = "http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000"async def main(): timeout = aiohttp.ClientTimeout(total=30) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get("https://api.ipify.org?format=json", proxy=PROXY) as r: print(await r.json()) # {"ip": "…"}asyncio.run(main()) - 03
Many tasks, one session, one sticky handle per task
Because the proxy is a per-request argument, one
ClientSessioncan serve tasks that each hold their own exit through a sticky session.TCPConnector(limit=…)caps open connections overall; the semaphore caps tasks.tasks.py26 linesimport asyncioimport secretsimport aiohttpasync def crawl_listing(session, start_url, pages=3): proxy = f"http://USERNAME_pool-arcfast_country-de_session-{secrets.token_hex(4)}_sesstime-10:PASSWORD@proxy.arcproxies.io:8000" statuses = [] for page in range(1, pages + 1): async with session.get(start_url, params={"page": page}, proxy=proxy) as r: await r.read() statuses.append(r.status) return statusesasync def main(starts): connector = aiohttp.TCPConnector(limit=50) limit = asyncio.Semaphore(10) async with aiohttp.ClientSession(connector=connector, timeout=aiohttp.ClientTimeout(total=30)) as session: async def guarded(url): async with limit: return await crawl_listing(session, url) print(await asyncio.gather(*(guarded(u) for u in starts)))asyncio.run(main(["https://example.com/c/lamps", "https://example.com/c/chairs"])) - 04
Geo-target per request
The username is per request too, so each call can pick its country, or city from the published list.
geo.py12 linesMARKETS = { "pl-warsaw": "USERNAME_pool-arcfast_country-pl_city-warsaw", "it-milan": "USERNAME_pool-arcfast_country-it_city-milan", "de": "USERNAME_pool-arcfast_country-de",}async def prices(session, url): out = {} for market, user in MARKETS.items(): async with session.get(url, proxy=f"http://{user}:PASSWORD@proxy.arcproxies.io:8000") as r: out[market] = r.status return out - 05
Use SOCKS5 through aiohttp-socks
aiohttp-socks replaces the connector, so the proxy applies to the whole session; a different SOCKS identity means a different session. For SOCKS5, the connector resolves hostnames at the proxy by default (
rdns=True).socks.py9 linesimport aiohttpfrom aiohttp_socks import ProxyConnectorasync def main(): connector = ProxyConnector.from_url("socks5://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000") async with aiohttp.ClientSession(connector=connector) as session: async with session.get("https://api.ipify.org?format=json") as r: print(await r.json()) - 06
Handle 407, timeouts and 429 without leaking the password
ClientHttpProxyErrorcarries the proxy's status. Its string form includes the proxy URL with the password, so log the status, not the exception.fetch.py22 linesimport asyncioimport secretsimport aiohttpasync def fetch(session, url, attempts=4): for attempt in range(attempts): proxy = f"http://USERNAME_pool-arcfast_country-de_session-{secrets.token_hex(4)}_sesstime-5:PASSWORD@proxy.arcproxies.io:8000" try: async with session.get(url, proxy=proxy) as r: if r.status == 407: raise RuntimeError("proxy rejected the credentials") if r.status != 429 and r.status < 500: return await r.text() except aiohttp.ClientHttpProxyError as err: if err.status == 407: raise RuntimeError("proxy rejected the credentials") from None print(f"proxy error {err.status}") # not str(err): it contains the URL except (aiohttp.ClientConnectionError, asyncio.TimeoutError): pass await asyncio.sleep(2 ** attempt) raise RuntimeError(f"gave up on {url}")
proxy=, proxy_auth= and the session default
| Option | Where | Status |
|---|---|---|
proxy="http://user:pass@host:port" | Per request | Documented; the usual choice |
ClientSession(proxy=…) | Session default | Documented; a per-request proxy= still overrides it |
proxy_auth=aiohttp.BasicAuth(…) | Per request | Deprecated since 3.14, to be removed in 4.0 |
trust_env=True | Session | Reads proxy environment variables, and credentials from ~/.netrc |
aiohttp proxies HTTPS targets through a CONNECT tunnel, so the TLS session runs from your code to the site and the gateway only sees the hostname.
Rotating vs sticky with aiohttp
A username without _session- lets each new connection take a new exit, but aiohttp keeps connections alive in the connector pool, and a tunnel to one host stays on one exit for its lifetime. Requests with different credentials do not share pooled connections, so a fresh handle per request is a reliable way to get a fresh IP, and a fixed sticky handle a reliable way to keep one. Rotating vs sticky sessions covers which one a job needs.
The problem
Common pitfalls
Logging the exception text
str(ClientHttpProxyError) includes the proxy URL with the password. Log err.status and the target URL instead.
Expecting environment variables to work
Unlike requests, aiohttp ignores HTTPS_PROXY unless the session has trust_env=True. Pass proxy= explicitly.
SOCKS URLs in proxy=
proxy= only speaks HTTP to the proxy; in our test a socks5:// URL there was still sent an HTTP CONNECT, not a SOCKS handshake. For SOCKS5, use aiohttp_socks.ProxyConnector.
No timeout on the session
Set ClientTimeout(total=…) on the session; a slow exit should cost one attempt, not a stuck task.
FAQ
Questions
Something not covered? Write to contact@arcproxies.io.
How do I use a proxy with authentication in aiohttp?
Put the credentials in the proxy URL: session.get(url, proxy="http://user:pass@host:port"). The proxy_auth= argument still exists but is deprecated since aiohttp 3.14.
Can I set one proxy for the whole aiohttp session?
Yes: aiohttp.ClientSession(proxy="http://user:pass@host:port") sets a default that each request can still override with its own proxy=.
Does aiohttp support SOCKS5 proxies?
Not natively. Install aiohttp-socks and create the session with connector=ProxyConnector.from_url("socks5://user:pass@host:port"). The connector applies to the whole session.
What is ClientHttpProxyError 407 in aiohttp?
The proxy refused to open the tunnel because the credentials were missing or wrong. Check the username and password, URL-encoding special characters in the password. It is not worth retrying.
Sources & review
Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.
- [1]aiohttp: Advanced client usage: Proxy support
- [2]aiohttp: Client reference: ClientSession
- [3]GitHub, romis2012/aiohttp-socks: aiohttp-socks README
Next
What people build with it
Integrations
Other http clients
Get started
Get credentials and paste them in.
Sign up, buy a gigabyte, generate a username in the dashboard. Code WELCOME15 takes 15% off your first purchase; bandwidth never expires.