All posts
guides8 min readOct 24, 2026, 09:01 AM

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 AsyncClient and 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. Use http://.

aiohttp:

  • ClientHttpProxyError, 407: wrong username or password.
  • ClientProxyConnectionError: wrong host or port.
  • ServerDisconnectedError: a socks5:// URL passed to proxy=. 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.

Written by NyxonProxies TeamGet 250MB free with Discord