Rotação de proxies assíncrona em Python: httpx e aiohttp (Guia 2026)

Publicado em 12 de junho de 2026 · ≈10 min de leitura

O scraping assíncrono é onde as configurações de proxy silenciosamente desmoronam. Com requests você rotaciona um IP por chamada e segue em frente; com asyncio você de repente tem 200 requisições em voo, todas precisando do seu próprio exit, da sua própria afinidade de sessão e da sua própria retentativa quando um alvo lança um 403. Erre nisso e você ou martela um único IP até o banimento ou destrói toda sessão de login ao rotacionar no meio do fluxo.

Este guia mostra os padrões que realmente se sustentam sob concorrência: rotação por requisição em httpx e aiohttp, um pool de workers acotado, sessões sticky para fluxos com estado e retentativas conscientes de bloqueio — com código funcional que você pode colar.

Por que o assíncrono muda o problema do proxy

Um scraper síncrono toca em um proxy por vez, então "rotacionar a cada requisição" é trivialmente correto. O assíncrono quebra três premissas de uma vez:

httpx: rotação de proxy por requisição

O httpx.AsyncClient vincula um proxy por cliente, então, para rotacionar, você escolhe o proxy por requisição e roteia através de um pequeno pool de clientes indexado por exit:

import asyncio, itertools, httpx

PROXIES = [
    "socks5h://USERNAME:[email protected]:913",
    "socks5h://USERNAME:[email protected]:913",
    "socks5h://USERNAME:[email protected]:913",
]
pool = itertools.cycle(PROXIES)

# One reusable client per exit (connection pooling stays intact)
clients = {p: httpx.AsyncClient(proxy=p, timeout=30) for p in PROXIES}

async def fetch(url):
    proxy = next(pool)
    r = await clients[proxy].get(url)
    return r.status_code, r.text

async def main(urls):
    results = await asyncio.gather(*(fetch(u) for u in urls))
    for client in clients.values():
        await client.aclose()
    return results

Reutilizar um cliente por exit mantém o HTTP/2 e o pooling de conexão vivos em vez de pagar um novo handshake a cada requisição. Note que httpx>=0.28 renomeou proxies= para proxy= no cliente.

aiohttp: rotação via requisição

O aiohttp é mais simples aqui — uma única ClientSession aceita um argumento proxy= por requisição, então você rotaciona inline:

import asyncio, itertools, aiohttp

pool = itertools.cycle(PROXIES)  # http:// or socks5h:// (needs aiohttp-socks for SOCKS)

async def fetch(session, url):
    proxy = next(pool)
    async with session.get(url, proxy=proxy, timeout=aiohttp.ClientTimeout(total=30)) as r:
        return r.status, await r.text()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

O aiohttp fala proxies HTTP nativamente; para socks5h:// adicione o aiohttp-socks e passe um ProxyConnector.

Limite a concorrência com um semáforo

Um gather sem limites é a forma mais rápida de ter todos os IPs do seu pool sinalizados de uma vez. Limite-o para que cada exit carregue um número de requisições paralelas plausível para um humano:

sem = asyncio.Semaphore(10)   # at most 10 in flight

async def fetch_capped(session, url):
    async with sem:
        return await fetch(session, url)

Regra prática: mantenha a concorrência em ou abaixo do número de exits distintos que você tem, para não estar empilhando muitas requisições paralelas em um único IP residencial.

Sessões sticky para fluxos com estado

A rotação serve para entrar; as sessões sticky servem para permanecer dentro. Um login, um carrinho, qualquer coisa vinculada a cookies deve manter um exit para toda a sequência. Fixe o proxy por tarefa em vez de por requisição:

async def run_account(account, proxy):
    # Same exit IP for every step of this account's flow
    async with httpx.AsyncClient(proxy=proxy, timeout=30) as client:
        await client.post("https://target.site/login", data=account.creds)
        await client.get("https://target.site/dashboard")
        await client.post("https://target.site/cart", json=account.order)

async def main(accounts):
    # One sticky exit per account, accounts run concurrently
    await asyncio.gather(*(run_account(a, p)
                           for a, p in zip(accounts, itertools.cycle(PROXIES))))

Use um provedor que suporte sessões residenciais sticky, para que o mesmo exit seja mantido durante toda a vida da tarefa, e não silenciosamente rotacionado por baixo dos panos.

Retentativas conscientes de bloqueio

Sob concorrência, você não pode confiar em um 200 — sistemas anti-bot retornam um 200 com um corpo de desafio. Detecte bloqueios e tente novamente a única tarefa que falhou em um exit novo:

BLOCK_MARKERS = ("just a moment", "/cdn-cgi/challenge-platform",
                 "datadome", "px-captcha", "access denied")

def looks_blocked(status, text):
    return status in (403, 429, 503) or any(m in text[:4000].lower() for m in BLOCK_MARKERS)

async def fetch_retry(url, attempts=4):
    for _ in range(attempts):
        proxy = next(pool)
        async with httpx.AsyncClient(proxy=proxy, timeout=30) as c:
            r = await c.get(url)
            if not looks_blocked(r.status_code, r.text):
                return r
        await asyncio.sleep(0.5)
    raise RuntimeError(f"blocked after {attempts} exits: {url}")

A fingerprint ainda precisa combinar

Um detalhe que o assíncrono não resolve: httpx e aiohttp ambos assentam sobre a stack TLS do Python, então enviam um JA3 que nenhum navegador real produz. Um pool de rotação perfeito em um IP residencial limpo ainda é identificado como automação pela fingerprint no handshake. Combine a rotação assíncrona com a impersonação de TLS — veja Como Burlar o TLS Fingerprinting com curl_cffi, e verifique o JA3 + ASN do seu exit em www.jibaoproxy.com/tools/fingerprint.html.

Checklist

Para todos os produtos de IP · um enorme pool de nós disponíveis a qualquer momento

Cadastre-se agora e ganhe até 100% de reembolso na sua recarga

Novos usuários recebem 500MB ao se cadastrar, mais um bônus na primeira recarga. Oferta por tempo limitado.