Using SOCKS5 Proxies with aiohttp
aiohttp's proxy argument only speaks HTTP. Install aiohttp-socks, hand the session a ProxyConnector, and set rdns so DNS resolves at the proxy.
aiohttp's proxy= argument handles HTTP proxies only; passing a socks5:// URL raises ValueError: Only http proxies are supported. SOCKS5 goes in through a connector instead, supplied by the aiohttp-socks package.
pip install aiohttp aiohttp-socksimport asyncio
import aiohttp
from aiohttp_socks import ProxyConnector
async def main():
connector = ProxyConnector.from_url("socks5h://USERNAME:PASSWORD@HOST:PORT")
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get("https://api.ipify.org?format=json") as r:
print(await r.json())
asyncio.run(main())Use the SOCKS5 port, not the HTTP port; the two are separate listeners on the same credential. On the dashboard's Proxies page, switch the Connect card's protocol to SOCKS5 and it shows the right port (the residential card lists a separate SOCKS5 Port field instead). Write the URL with the socks5h:// scheme, for the reason below. SOCKS5 is available on every paid product on the pricing page; the free tier is HTTP only. If the printed address is the proxy's rather than your own, you are done; the rest of this page covers the choices that decide whether it stays that way.
Why the connector, not the proxy argument
An HTTP proxy is reached over HTTP: the client sends CONNECT target:443 and the proxy relays bytes from there. SOCKS5 negotiates in binary before any HTTP exists, so aiohttp cannot express it in a request option. A connector is aiohttp's socket factory, responsible for producing a connected transport for a host, which is exactly the layer where the SOCKS5 handshake belongs. Once the session has a ProxyConnector, every request through it is proxied and individual calls need no proxy= at all.
Where DNS happens: rdns
socks5:// and socks5h:// differ in who resolves the hostname. With socks5:// your machine resolves it and sends the proxy an IP address. With socks5h:// the hostname travels to the proxy and is resolved there. The same choice is available as an argument:
from aiohttp_socks import ProxyConnector, ProxyType
connector = ProxyConnector(
proxy_type=ProxyType.SOCKS5,
host="HOST",
port=PORT,
username="USERNAME",
password="PASSWORD",
rdns=True, # resolve at the proxy
)rdns=True (or socks5h://) is the setting you want in almost every case. Local resolution sends every hostname you visit to your own resolver, so the lookups are visible on your network even though the requests are not. It also resolves geographically balanced names for your location rather than the exit's, so a request through an exit in one country can land on a server chosen for another. And a name that only resolves inside the proxy's network cannot be resolved by you at all. The protocol-level explanation, with curl and Node examples, is in the SOCKS5 setup guide.
One session, built once
A session per request performs a full SOCKS5 handshake and a fresh TCP connection for every call, then discards both. That multiplies your connection count by your request count, adds handshake bytes on a metered plan, and is the quickest way to hit a per-address connection cap your real concurrency is nowhere near. Build the session once and pass it down:
import aiohttp
from aiohttp_socks import ProxyConnector
class Client:
def __init__(self, proxy_url: str):
self._proxy_url = proxy_url
self._session: aiohttp.ClientSession | None = None
async def __aenter__(self):
connector = ProxyConnector.from_url(self._proxy_url, limit=20, ttl_dns_cache=300)
self._session = aiohttp.ClientSession(
connector=connector,
timeout=aiohttp.ClientTimeout(total=45, connect=15, sock_read=30),
)
return self
async def __aexit__(self, *exc):
await self._session.close()
async def get(self, url: str) -> str:
async with self._session.get(url) as r:
r.raise_for_status()
return await r.text()limit=20 caps concurrent connections through the connector. Without it, asyncio.gather over a few thousand URLs opens a few thousand sockets at once, and the resulting failures look like the proxy refusing you.
The timeout is split on purpose. connect covers reaching the proxy and completing the handshake; sock_read covers silence between chunks once data is flowing. A single total timeout cannot tell a proxy that never finishes negotiating from a target that is slow to send a large body, and through a proxy there are two hops that can stall.
Rotating across a pool
With several exits, hold one session per proxy and cycle them:
import itertools
class Pool:
def __init__(self, proxy_urls: list[str]):
self._sessions = [
aiohttp.ClientSession(connector=ProxyConnector.from_url(u, limit=10))
for u in proxy_urls
]
self._cycle = itertools.cycle(self._sessions)
def next(self) -> aiohttp.ClientSession:
return next(self._cycle)
async def close(self):
for s in self._sessions:
await s.close()That pattern is for a list of static proxies. A rotating or residential gateway is one URL, so one session is enough, and the exit is chosen per SOCKS5 connection: a pooled connection keeps its exit for as long as it stays open. For a fresh exit on every request, stop the connector reusing sockets with ProxyConnector.from_url(url, force_close=True); to hold one exit across a sequence of connections instead, add a session id to the username.
When a target has set a cookie you need to keep, pin that work to one session for the life of that state; sticky versus rotating sessions explains where the line falls.
Errors
ProxyConnectionError: the SOCKS5 endpoint did not accept a TCP connection. Check the host and port; this happens before authentication, so credentials are not implicated.ProxyErrorafter connecting: usually authentication. SOCKS5 has no 407, so the negotiation simply fails. Check that you are on the SOCKS5 port rather than the HTTP port (the HTTP listener answers the handshake with something the client cannot parse), then check the credential, and on a free account remember that SOCKS5 needs a paid plan.ProxyTimeoutError: the handshake began and did not finish. Try a different exit before retrying the same one.- An HTTP
407from the target: the request did not go where you expected. Fixing 407 Proxy Authentication Required covers how credentials get mangled on the way out.
Confirm the proxy is applied
The one check worth trusting is the address the destination reports. Run the first example in this guide against https://api.ipify.org?format=json: if it prints your own address, a session somewhere in the codebase was created without the connector, which is the case for keeping a single client object.
Still choosing a product for the code? SOCKS5 proxies for aiohttp: which proxy type to use matches rotating, residential, dedicated and shared to the jobs aiohttp usually does.