Proxies in httpx and aiohttp: async Python with authentication, SOCKS5 and rotation
httpx dropped proxies= for proxy=, and aiohttp ignores environment variables by default and needs a plugin for SOCKS5. Working async code for both, credentials with special characters, why parallel requests get different IPs, sticky sessions and every exception explained.

Async Python has two popular HTTP clients, httpx and aiohttp, and they handle proxies differently enough that code copied from one breaks in the other. Here is the minimal working version of each.
httpx:
import httpx
proxy = "http://USERNAME:[email protected]:8080"
r = httpx.get("https://api.ipify.org?format=json", proxy=proxy)
print(r.json())aiohttp:
import asyncio
import aiohttp
PROXY = "http://USERNAME:[email protected]:8080"
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.ipify.org?format=json", proxy=PROXY) as r:
print(await r.json())
asyncio.run(main())Below: the httpx argument that changed, routing rules, environment variables, SOCKS5, credentials, async concurrency, why parallel and sequential requests get different IPs, sticky sessions, and the exception each mistake raises. Everything was tested with httpx 0.28.1, aiohttp 3.14.5 and aiohttp-socks 0.12 on Python 3.14.
httpx: proxy, not proxies
httpx takes a single proxy argument, on the top-level functions and on Client and AsyncClient. Older tutorials use proxies=, which httpx 0.28 removed, so that code now fails immediately:
TypeError: Client.__init__() got an unexpected keyword argument 'proxies'For a long-running script, create one client and reuse it:
import httpx
with httpx.Client(proxy="http://USERNAME:[email protected]:8080") as client:
r = client.get("https://api.ipify.org?format=json")
print(r.json())To route some traffic differently, use mounts, which map URL patterns to transports. A value of None sends matching requests directly:
import httpx
mounts = {
"https://": httpx.HTTPTransport(proxy="http://USERNAME:[email protected]:8080"),
"all://127.0.0.1": None,
}
with httpx.Client(mounts=mounts) as client:
print(client.get("https://api.ipify.org?format=json").json())httpx and environment variables
httpx reads proxy environment variables by default. In our tests it used HTTPS_PROXY for HTTPS sites and also honoured ALL_PROXY. With only HTTP_PROXY set, HTTPS requests went direct. A proxy= argument in the code took priority over the environment, and trust_env=False turned the environment off completely.
The proxy environment variables guide compares these rules with curl, Requests, aiohttp, Node and Go.
httpx with SOCKS5
SOCKS support needs an extra package. Without it you get:
ImportError: Using SOCKS proxy, but the 'socksio' package is not installed. Make sure to install httpx using `pip install httpx[socks]`.Install it with the quotes, which most shells need around square brackets:
pip install "httpx[socks]"Then use a socks5:// URL:
import httpx
r = httpx.get(
"https://api.ipify.org?format=json",
proxy="socks5://USERNAME:[email protected]:1080",
)
print(r.json())Unlike Requests, httpx sent the hostname to the proxy with plain socks5://, so DNS was resolved on the proxy side; socks5h:// behaved the same.
aiohttp: per request or per session
aiohttp takes proxy= on each request, as in the first example, or once on the session as a default for every request:
import asyncio
import aiohttp
async def main():
async with aiohttp.ClientSession(proxy="http://USERNAME:[email protected]:8080") as session:
async with session.get("https://api.ipify.org?format=json") as r:
print(await r.json())
asyncio.run(main())Older examples pass credentials with proxy_auth=aiohttp.BasicAuth(...). That still works in 3.14, but it prints two deprecation warnings, one for each name, saying both will be removed in aiohttp 4. The warning suggests a header instead, which we confirmed runs cleanly with warnings turned into errors:
headers = {"Proxy-Authorization": aiohttp.encode_basic_auth("USERNAME", "PASSWORD")}
async with session.get(url, proxy="http://geo.gw.nyxon.sh:8080", proxy_headers=headers) as r:
print(r.status)Credentials inside the proxy URL also work without any warning.
aiohttp and environment variables
aiohttp ignores proxy environment variables unless you create the session with trust_env=True. With it, aiohttp used HTTPS_PROXY for HTTPS sites. It did not use ALL_PROXY, and with only HTTP_PROXY set, HTTPS requests went direct.
aiohttp with SOCKS5: aiohttp-socks
aiohttp's own proxy= speaks HTTP only. Point it at a SOCKS5 port and it sends an HTTP request to a SOCKS server, which hangs up; the error, ServerDisconnectedError: Server disconnected, gives no hint of the real cause.
Use the aiohttp-socks package (pip install aiohttp-socks), which plugs in as the connector:
import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector
async def main():
connector = ProxyConnector.from_url("socks5://USERNAME:[email protected]:1080")
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())It sent the hostname to the proxy by default. It does not accept socks5h://: that raised ValueError: Invalid scheme component: socks5h.
Passwords with special characters
Characters such as @, :, / and # break the proxy URL. With a password of p@ss:w/rd#1 pasted raw, httpx raised httpx.InvalidURL: Invalid port: 'w' and aiohttp raised InvalidURL. Percent-encode the credentials and both work:
import os
from urllib.parse import quote
user = os.environ["PROXY_USER"]
password = quote(os.environ["PROXY_PASS"], safe="")
proxy = f"http://{user}:{password}@geo.gw.nyxon.sh:8080"In httpx you can also skip the encoding by passing an httpx.Proxy object, which takes the credentials separately as auth=(user, password). In aiohttp, the proxy_headers form above does the same.
Parallel requests, sequential requests and IPs
On a rotating username, each new connection to the proxy gets a new IP. HTTPS traffic travels through a tunnel that stays on one exit IP for as long as it is open, and both clients keep connections open for reuse.
What we measured, five requests to the same site:
- One client or session, one after another: one tunnel, so one IP for all five.
- One client or session, in parallel: five tunnels, with httpx
AsyncClientand with aiohttp. - Keep-alive turned off: five tunnels, even one after another.
So if you need a new IP for every request, turn keep-alive off. In httpx:
import httpx
limits = httpx.Limits(max_keepalive_connections=0)
proxy = "http://USERNAME:[email protected]:8080"
with httpx.Client(proxy=proxy, limits=limits) as client:
for _ in range(3):
print(client.get("https://api.ipify.org").text)In aiohttp, give the session a TCPConnector created with force_close=True. If you want the opposite, the same IP across requests, do not rely on keep-alive; use a sticky session.
Sticky sessions and concurrency
NyxonProxies reads targeting and session options from the username: -country- for the exit country, -session- with an ID to hold one IP, and -time- for how long, in minutes (1 to 120). In async code, give each task its own session ID, and cap how many run at once with a semaphore:
import asyncio
import secrets
import aiohttp
GATEWAY = "geo.gw.nyxon.sh:8080"
limit = asyncio.Semaphore(10)
def sticky_proxy(country, minutes=10):
sid = secrets.token_hex(4)
return f"http://USERNAME-country-{country}-session-{sid}-time-{minutes}:PASSWORD@{GATEWAY}"
async def check(session, country):
proxy = sticky_proxy(country)
async with limit:
for _ in range(2):
async with session.get("https://api.ipify.org?format=json", proxy=proxy) as r:
print(country, (await r.json())["ip"])
async def main():
async with aiohttp.ClientSession() as session:
await asyncio.gather(*(check(session, c) for c in ["us", "de", "gb"]))
asyncio.run(main())Residential sessions last up to 120 minutes. The Generate page builds these usernames for you, and the sessions guide explains each option.
Errors and what they mean
httpx:
- httpx.ProxyError, 407: wrong username or password, on an HTTPS site.
- A 407 response, no exception: the same mistake on a plain HTTP site. Call
raise_for_status()to catch it. - httpx.InvalidURL: special characters in the credentials were not encoded.
- httpx.ConnectError, refused: wrong host or port. The wording depends on the operating system.
- httpx.ConnectError, SSL EOF:
https://in front of the HTTP port. Usehttp://.
aiohttp:
- ClientHttpProxyError, 407: wrong username or password.
- ClientProxyConnectionError: wrong host or port.
- ServerDisconnectedError: a
socks5://URL passed toproxy=. Use aiohttp-socks. - InvalidURL: special characters in the credentials were not encoded.
One warning for logging: aiohttp's ClientHttpProxyError and InvalidURL messages contain the full proxy URL, password included. Log your own message rather than the exception text. The proxy error codes guide covers the status codes in detail.
FAQ
Why does httpx say it got an unexpected keyword argument 'proxies'? httpx 0.28 removed proxies=. Use proxy= for one proxy, or mounts for different routes.
Does aiohttp support SOCKS5? Not by itself. Use aiohttp-socks and its ProxyConnector.
Why do my async requests come from different IPs? Parallel requests open separate tunnels, and each tunnel gets its own IP on a rotating username. Use a sticky session when requests must share an IP.
Should I use httpx or aiohttp for scraping through proxies? Both work. httpx reads environment variables by default, supports SOCKS5 with an extra install and has a matching sync API; aiohttp needs trust_env=True for environment variables and aiohttp-socks for SOCKS5.
Try it
Create a NyxonProxies account, link Discord under Freebies for 250MB of free Residential Budget traffic, and paste the username and password from the Generate page into any example above. Prices for every proxy type are on the pricing page. For the synchronous Requests library, see the Python Requests guide.