To use a proxy with aiohttp, pass its URL as proxy= on a request or on the ClientSession, with the login inside the URL or in a Proxy-Authorization header. proxy_auth=aiohttp.BasicAuth still works but is deprecated since aiohttp 3.14. SOCKS5 needs aiohttp-socks. Give every asyncio task its own sticky session. Real code and output below.
aiohttp is the asyncio HTTP client for Python: many requests in flight at once from one process, which is exactly where a proxy plan earns its keep and where small mistakes multiply. The Residential Premium scripts and every error on this page were run on 24 October 2026 with Python 3.13.6, aiohttp 3.14.5 and aiohttp_socks 0.12.0, through a QuanticData plan with exits in Chile; ISP and mobile lines use the same code with the values from their own panels. The outputs are copied from the terminal; only the credentials are replaced by USER and PASS and the exit IPs are masked. If your code is synchronous, the same proxy works with a proxy with Python requests.
What you need before you start
- Python 3.10 or newer and aiohttp. aiohttp 3.14.5 requires Python 3.10 or later:
pip install aiohttp. For SOCKS5 addpip install aiohttp-socks. - A proxy plan. Residential Premium, ISP or mobile: all three work with aiohttp. The table further down says which one fits which job.
- The proxy URL. In the QuanticData dashboard open My proxies, then your plan. In the Endpoint generator set Output format to http://user:pass@host:port: the Preview is then the exact string aiohttp takes as proxy=.
The proxy format aiohttp expects
aiohttp reads a proxy as a URL: scheme, login and password, host and port. The same QuanticData proxy in the two shapes you will use:
# credentials inside the URL: works on every aiohttp version, no warning
http://USER-country-cl-sid-aiohttp01-ttl-60m:PASS@residentialpremium.quantumproxies.io:8080
# the gateway alone, with the credentials passed separately
http://residentialpremium.quantumproxies.io:8080
Four details decide whether it connects:
- The scheme is http://, even for https:// sites. aiohttp asks the gateway for a CONNECT tunnel and runs TLS to the site inside it. A proxy URL that starts with https:// makes aiohttp speak TLS to the gateway itself, and the request fails with an SSL WRONG_VERSION_NUMBER error.
- Port 8080 is HTTP, port 1080 is SOCKS5. aiohttp's own proxy= speaks HTTP only; SOCKS5 goes through aiohttp-socks, further down.
- The username is the whole string.
-country-clpicks the exit country,-sid-aiohttp01names the session and-ttl-60mkeeps it for 60 minutes. A login cut at the first dash still authenticates, but every request then leaves from a random country on a new IP. - Reserved characters need escaping. The credentials the generator shows are letters and digits and go into the URL as they are. A password of your own with @, : or / has to be percent-encoded, for example with urllib.parse.quote, before it goes into a URL.

Step by step: proxy= and proxy_auth= on a request
- Generate the line. In the Endpoint generator choose the Country, set Session to Sticky, set Session TTL (minutes) to at least the length of the job, keep Protocol on HTTP and pick the output format you want. Connection details shows host, port, username and password one by one, which is what proxy_auth= needs.
- Write the request. proxy= gets the gateway URL, proxy_auth= gets an aiohttp.BasicAuth with the username and the password.
- Run it and read the exit. https://ipinfo.io/json answers with the IP, city, region and time zone the site sees. The country has to match the
-country-in the username. - On aiohttp 3.14 or later, move the credentials. Put them in a Proxy-Authorization header or in the URL, and the deprecation warnings go away.

The script sends the same request twice, first with proxy_auth=, then with the header that aiohttp 3.14 recommends:
import asyncio
import aiohttp
PROXY = "http://residentialpremium.quantumproxies.io:8080"
LOGIN = "USER-country-cl-sid-aiohttp01-ttl-60m"
PASSWORD = "PASS"
URL = "https://ipinfo.io/json"
def where(info):
return f'{info["ip"]} {info["city"]}, {info["region"]} {info["timezone"]}'
async def main():
async with aiohttp.ClientSession() as session:
auth = aiohttp.BasicAuth(LOGIN, PASSWORD)
async with session.get(URL, proxy=PROXY, proxy_auth=auth) as r:
print("proxy_auth ", r.status, where(await r.json()))
token = aiohttp.encode_basic_auth(LOGIN, PASSWORD)
headers = {"Proxy-Authorization": token}
async with session.get(URL, proxy=PROXY, proxy_headers=headers) as r:
print("proxy_headers", r.status, where(await r.json()))
asyncio.run(main())
What it printed:
/Users/me/aiohttp-demo/proxy_auth.py:16: DeprecationWarning: BasicAuth is deprecated and will be removed in aiohttp 4.0; use aiohttp.encode_basic_auth() with headers={'Authorization': ...} instead
auth = aiohttp.BasicAuth(LOGIN, PASSWORD)
/Users/me/aiohttp-demo/proxy_auth.py:17: DeprecationWarning: The 'proxy_auth' parameter is deprecated and will be removed in v4; pass proxy_headers={'Proxy-Authorization': aiohttp.encode_basic_auth(login, password)} instead
async with session.get(URL, proxy=PROXY, proxy_auth=auth) as r:
proxy_auth 200 148.222.•••.••• Santiago, Santiago Metropolitan America/Santiago
proxy_headers 200 148.222.•••.••• Santiago, Santiago Metropolitan America/Santiago

Both requests went out through the same Chilean IP because they carry the same session ID, aiohttp01. The two warnings are aiohttp 3.14 telling you that the BasicAuth class and the proxy_auth parameter will be removed in aiohttp 4.0. Nothing breaks today, but code that should outlive the next major release needs one of the other two ways:
| How the credentials travel | Code | aiohttp versions |
|---|---|---|
| Inside the proxy URL | proxy="http://USER-...:PASS@host:8080" | Every version, no warning |
| proxy_auth= | proxy_auth=aiohttp.BasicAuth(login, password) | Works; DeprecationWarning from 3.14, removal planned for 4.0 |
| Proxy-Authorization header | proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(login, password)} | 3.14 and later: encode_basic_auth() was added in 3.14.0 |
One detail from the aiohttp changelog: encode_basic_auth() encodes in UTF-8, BasicAuth defaulted to latin-1. With the letters-and-digits credentials of the generator the bytes are identical.
Session-level settings: one proxy for every request
Since aiohttp 3.11.0 the ClientSession itself accepts proxy= and proxy_auth=, so every request of the session goes through the proxy without repeating it. A proxy= on a single request still overrides the session's.
proxy = "http://USER-country-cl-sid-aiohttp01-ttl-60m:PASS@residentialpremium.quantumproxies.io:8080"
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(proxy=proxy, timeout=timeout) as session:
async with session.get("https://ipinfo.io/json") as r:
print(r.status, await r.json())
- Put the credentials in the URL at session level. ClientSession has no proxy_headers parameter in aiohttp 3.14.5:
ClientSession(proxy=..., proxy_headers=...)fails withTypeError: ClientSession.__init__() got an unexpected keyword argument 'proxy_headers', even though an example in the documentation passes it. The header works on each request; the URL works everywhere. - Set a timeout. aiohttp's default is 300 seconds in total and 30 seconds to connect. Through a proxy a stalled request then holds its slot for five minutes; 30 seconds in total is a sensible start for page fetches.
- Environment variables are off by default. Unlike requests, aiohttp ignores HTTP_PROXY and HTTPS_PROXY unless the session is created with
trust_env=True. Then it reads them with urllib's getproxies(), credentials in the URL included, and skips the hosts listed in NO_PROXY. In our run, HTTPS_PROXY set to the Chilean line plus trust_env=True returned 200 from Chile.
Concurrency: one sticky session per task
The reason to use aiohttp is concurrency, and that is where the proxy needs a rule: one sticky session per task. A task that logs in, follows a cart or walks through pages should keep one IP from its first request to its last; two tasks should never share one. The session ID in the username does exactly that, so the code only has to build one proxy URL per task:
import asyncio
import aiohttp
GATEWAY = "residentialpremium.quantumproxies.io:8080"
USER, PASSWORD = "USER", "PASS"
URL = "https://ipinfo.io/json"
def proxy_url(sid, country="cl", ttl=60):
login = f"{USER}-country-{country}-sid-{sid}-ttl-{ttl}m"
return f"http://{login}:{PASSWORD}@{GATEWAY}"
async def task(n):
proxy = proxy_url(f"aiohttp{n:02d}")
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(proxy=proxy, timeout=timeout) as s:
for step in (1, 2):
async with s.get(URL) as r:
info = await r.json()
print(f"task {n} request {step}", info["ip"], info["city"])
async def main():
await asyncio.gather(*(task(n) for n in (1, 2, 3)))
asyncio.run(main())
task 3 request 1 190.114.•••.••• Santiago
task 1 request 1 148.222.•••.••• Santiago
task 2 request 1 190.5.•••.••• Chanco
task 3 request 2 190.114.•••.••• Santiago
task 1 request 2 148.222.•••.••• Santiago
task 2 request 2 190.5.•••.••• Chanco

Task 1 also came back on 148.222.•••.•••, the IP of the first script: same session ID, same IP, as long as the 60 minutes have not run out. Why a ClientSession per task and not one shared session with a different proxy= per request? Both keep the IPs apart: aiohttp's connection pool keys every connection by its proxy, so two session IDs never share a socket. But a ClientSession also holds the cookies, and a task that behaves like one visitor should not see the cookies of another.
For many tasks, cap how many run at once. A semaphore keeps, here, ten sessions open at a time over fifty tasks; each ClientSession also has its own connector with the default limit of 100 connections:
async def task(n, sem):
async with sem, aiohttp.ClientSession(proxy=proxy_url(f"aiohttp{n:02d}")) as s:
async with s.get(URL) as r:
return r.status
async def main():
sem = asyncio.Semaphore(10)
statuses = await asyncio.gather(*(task(n, sem) for n in range(1, 51)))
For a new IP on every request instead, leave -sid- out of the username (Session: Rotating in the generator). Pools of separate proxies and their rotation are covered in rotating proxies in Python.
SOCKS5 with aiohttp-socks
aiohttp has no SOCKS support of its own: a socks5:// URL in proxy= is spoken to as if it were HTTP and fails. The aiohttp-socks package adds a connector that does the SOCKS5 handshake; install it with pip install aiohttp-socks and give the connector to the session:
import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector
PROXY = ("socks5://USER-country-cl-sid-aiohttp04-ttl-60m:PASS"
"@residentialpremium.quantumproxies.io:1080")
async def main():
connector = ProxyConnector.from_url(PROXY)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get("https://ipinfo.io/json") as r:
info = await r.json()
print(r.status, info["ip"], info["city"], info["region"],
info["timezone"])
asyncio.run(main())
200 191.113.•••.••• Santiago Santiago Metropolitan America/Santiago

- One connector, one proxy, one session. The connector belongs to the ClientSession, so the one-session-per-task rule above comes for free: build a connector per task with its own session ID.
- DNS through the proxy. For socks5 URLs aiohttp-socks lets the proxy resolve the host name by default (rdns), so the site's name is not looked up on your network.
- Same login, other port. The username and password are the same as on HTTP; only the scheme and port 1080 change. More on when SOCKS5 is worth it on the page for SOCKS5 proxies.
Which proxy type for which aiohttp job
| Proxy type | In aiohttp | Good for |
|---|---|---|
| Residential Premium | One URL per task with a sticky -sid-; country, region, city or ISP in the username; HTTP 8080, SOCKS5 1080; sessions up to 1440 minutes | Most scraping and multi-step jobs: home IPs in the country you choose |
| ISP | Fixed IPs from Assigned IPs, Output format Proxy URL, pasted as proxy=; HTTP 50100, SOCKS5 50101 | Accounts and jobs that must keep the same address for months |
| Mobile | Same pattern as Premium with -type-mobile in the username; mobile.quantumproxies.io, HTTP 8080, SOCKS5 1080; IP whitelist available | Mobile-first sites and apps that expect carrier IPs |
ISP proxies in aiohttp
An ISP plan gives fixed IPs instead of a gateway. In the dashboard, Proxy endpoints, Assigned IPs lists them; set Output format to Proxy URL and every line is already a proxy= string: http://USER:PASS@IP:50100. Protocol HTTP / HTTPS uses port 50100; switching it to SOCKS5 changes the port in the list to 50101, for use with aiohttp-socks. There is no session ID and no lifetime: the IP is the line, so give each account or job its own line and its own ClientSession.


Mobile proxies in aiohttp
Mobile lines work like Residential Premium with -type-mobile in the username. In the plan's Endpoint generator choose the country, the Proxy host (gateway) closest to your server, Global, USA, EU or Asia, then Sticky and the protocol: HTTP on 8080 or SOCKS5 on 1080. The Preview shows the host to use.
MOBILE = ("http://USER-country-cl-type-mobile-sid-aiohttp07-ttl-60m:PASS"
"@mobile.quantumproxies.io:8080")
async with aiohttp.ClientSession(proxy=MOBILE) as session:
async with session.get("https://ipinfo.io/json") as r:
print(r.status, await r.json())
Mobile plans also accept IP whitelist authentication: add your server's IP in the generator, then use a line from Proxy list for a whitelisted IP as proxy="http://HOST:PORT", with no login at all. For a new mobile IP, change the session ID, for example aiohttp08 instead of aiohttp07. The plans are on the page for mobile proxies.

aiohttp proxy not working: errors and fixes
Each mistake below was reproduced on 24 October 2026 with aiohttp 3.14.5; the first column is what aiohttp actually raised.
| What aiohttp raises | Cause | Fix |
|---|---|---|
ClientHttpProxyError: 407, message='Proxy Authentication Required' | Wrong password, or the username cut or copied with a space | Copy both again from Connection details; check the plan is active with traffic left |
ClientResponseError: 400, message="Bad status line" with b'\x05\xff' | An http:// or socks5:// proxy= pointed at the SOCKS5 port 1080: aiohttp spoke HTTP to a SOCKS5 server | HTTP on 8080, or SOCKS5 on 1080 through aiohttp-socks |
IncompleteReadError: 0 bytes read on a total of 2 expected bytes, after about 15 seconds | aiohttp-socks pointed at the HTTP port 8080 | socks5:// with port 1080 |
ClientConnectorSSLError with WRONG_VERSION_NUMBER | The proxy URL starts with https:// | http:// for the proxy, even for https:// sites |
TypeError: ClientSession.__init__() got an unexpected keyword argument 'proxy_headers' | proxy_headers passed to the session | Credentials in the session's proxy URL, or proxy_headers on each request |
DeprecationWarning: BasicAuth is deprecated | aiohttp 3.14 or later with proxy_auth= | Proxy-Authorization header or credentials in the URL |
| Requests leave from your own IP | HTTPS_PROXY set but the session ignores it | trust_env=True, or proxy= in the code |
| The IP changes in the middle of a task | The session lifetime ended, or two tasks share one session ID | A longer -ttl- and a different -sid- in every task |

Look at the 407 again: the exception message repeats the whole proxy URL, password included. A logger that writes str(error) writes your proxy password into the logs. Log the status and the host instead, or strip the credentials first. The status itself is explained in 407 Proxy Authentication Required.
When aiohttp fails and you cannot tell whether the proxy or the code is at fault, test the same values outside Python. If this returns an address in the right country, the proxy works and the problem is in the script:
curl -x http://residentialpremium.quantumproxies.io:8080 \
-U "USER-country-cl-sid-test-ttl-10m:PASS" https://ipinfo.io/json
Certificate errors are a different family, covered in SSL certificate errors behind a proxy.
HTTP or SOCKS5 in aiohttp
For web scraping, HTTP on port 8080 is the simpler choice: it needs nothing beyond aiohttp, works with proxy= on requests and sessions, and every QuanticData network offers it. SOCKS5 on port 1080 needs aiohttp-socks and one connector per session, and is worth it when the host name should be resolved by the proxy or when the same proxy also serves tools that do not speak HTTP. If you also use the other async client, the httpx proxy guide covers its proxy= and mounts. The plans themselves are on the pages for QuanticData residential proxies and ISP proxies.
Before you scale up
A proxy changes where requests come from, not what a site allows. Keep to each site's terms and to the law where you work, keep the concurrency reasonable for the site you fetch from, and give every account its own session and its own IP.
Sources & further reading
- aiohttp documentation: Advanced Client Usage, Proxy support
- aiohttp documentation: Client Reference (ClientSession, BasicAuth, encode_basic_auth, TCPConnector)
- aiohttp changelog: 3.11.0 session proxy parameters, 3.14.0 BasicAuth deprecation
- aiohttp-socks on PyPI: ProxyConnector for SOCKS proxies
- Python documentation: urllib.request.getproxies
- RFC 1928: SOCKS Protocol Version 5