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-socks
import 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

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.