La biblioteca httpx se ha convertido en el estándar para solicitudes HTTP asíncronas en Python, pero al trabajar con proxies tiene matices que rompen los parsers en producción. Vamos a ver cómo configurar la rotación asíncrona de proxies, habilitar HTTP/2 y por qué su script falla cada 50-100 solicitudes.
Por qué httpx y no requests o aiohttp
requests es sincrónico y no soporta HTTP/2 de forma nativa. aiohttp es asíncrono, pero la configuración de proxies es menos amigable y la compatibilidad con HTTP/2 está completamente ausente. httpx aborda ambas cuestiones: un cliente sincrónico y asíncrono con una API unificada, soporte nativo de HTTP/2 a través de la extensión h2, y un manejo flexible de proxies a nivel de transporte.
Para tareas de scraping en marketplaces (Wildberries, Ozon, Avito), recolección de datos de APIs de redes sociales o monitoreo de precios de competidores, la asincronía es crítica: proporciona un aumento significativo en la velocidad en comparación con las solicitudes secuenciales. Pero sin una rotación adecuada de proxies y gestión de conexiones, la asincronía se convierte en una fuente de nuevos problemas: agotamiento del grupo de conexiones, corutinas colgadas, fugas de descriptores de archivos.
Un segundo punto importante: HTTP/2 puede multiplexar múltiples solicitudes en una sola conexión TCP, lo que reduce la carga en el servidor de destino y ayuda a eludir algunos sistemas anti-bots ingenuos que observan la cantidad de handshakes TCP desde una sola IP.
Configuración básica de proxies en httpx
Desde la versión httpx 0.26+, la sintaxis para configurar proxies ha cambiado: en lugar de un diccionario proxies, se utiliza el parámetro proxy. Esta es una fuente común de confusión si estás viendo tutoriales antiguos.
import httpx
import asyncio
PROXY_URL = "http://user:[email protected]:8000"
async def fetch(url: str):
async with httpx.AsyncClient(proxy=PROXY_URL, timeout=15.0) as client:
response = await client.get(url)
return response.status_code, response.text[:200]
asyncio.run(fetch("https://httpbin.org/ip"))
Si necesitas separar proxies para tráfico HTTP y HTTPS o configurar diferentes proxies para diferentes dominios, utiliza httpx.Client(mounts=...) — los transportes se montan según el patrón de URL:
mounts = {
"http://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
"https://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
}
async with httpx.AsyncClient(mounts=mounts) as client:
response = await client.get("https://example.com")
Para la autenticación con nombre de usuario y contraseña, el formato http://user:pass@host:port funciona de manera estable para proxies HTTP. Para SOCKS5, necesitarás instalar httpx[socks] adicionalmente, de lo contrario recibirás un ImportError en tiempo de ejecución, y no en la etapa de escritura del código — esta es una trampa en la que caen muchos.
Rotación asíncrona de proxies: código funcional
El principal error en la rotación de proxies es crear un nuevo AsyncClient para cada solicitud. Esto mata el rendimiento, porque el cliente mantiene un grupo de conexiones, y recrearlo anula todas las ventajas de las conexiones keep-alive. El enfoque correcto es crear un grupo de clientes por adelantado, uno por proxy, y distribuir las tareas entre ellos.
import httpx
import asyncio
import itertools
import random
PROXIES = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
class ProxyPool:
def __init__(self, proxies: list[str]):
self.clients = [
httpx.AsyncClient(proxy=p, timeout=httpx.Timeout(15.0, connect=5.0))
for p in proxies
]
self._cycle = itertools.cycle(self.clients)
def get_client(self) -> httpx.AsyncClient:
return next(self._cycle) # round-robin
# para selección aleatoria: random.choice(self.clients)
async def close_all(self):
for client in self.clients:
await client.aclose()
async def fetch_with_retry(pool: ProxyPool, url: str, retries: int = 3):
for attempt in range(retries):
client = pool.get_client()
try:
response = await client.get(url)
if response.status_code == 200:
return response.text
except (httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout):
await asyncio.sleep(0.5 * (attempt + 1))
continue
return None
async def main():
pool = ProxyPool(PROXIES)
urls = [f"https://example.com/page/{i}" for i in range(100)]
sem = asyncio.Semaphore(20) # limitamos el paralelismo
async def bound_fetch(url):
async with sem:
return await fetch_with_retry(pool, url)
results = await asyncio.gather(*(bound_fetch(u) for u in urls))
await pool.close_all()
return results
asyncio.run(main())
Este patrón resuelve tres problemas a la vez: reutiliza conexiones TCP a través de keep-alive, distribuye la carga entre proxies de manera round-robin y limita el paralelismo a través de un semáforo, para no sobrecargar el servidor de destino y evitar bloqueos por rate-limit.
Para parsers en producción que trabajan con Wildberries, Ozon o la API de Instagram, es importante que el grupo de proxies sea lo suficientemente grande y diverso geográficamente; de lo contrario, la rotación dentro de una misma subred no salvará de bloqueos. Aquí son adecuados los proxies residenciales, ya que tienen IP de usuarios reales y son menos propensos a caer en los filtros de sistemas anti-bots que analizan el ASN y la reputación del rango.
HTTP/2 en httpx: por qué y cómo habilitarlo
Por defecto, httpx utiliza HTTP/1.1. Para habilitar HTTP/2, necesitas instalar la extensión y pasar explícitamente el flag:
# pip install httpx[http2]
async with httpx.AsyncClient(http2=True, proxy=PROXY_URL) as client:
response = await client.get("https://example.com")
print(response.http_version) # esperamos "HTTP/2"
Un matiz importante: no todos los proxies soportan el tunneling de HTTP/2 correctamente. Algunos proxies de centros de datos baratos funcionan solo como proxies HTTP/1.1 incluso si el servidor final soporta HTTP/2; en tal caso, httpx retrocederá silenciosamente a HTTP/1.1 a través de un túnel CONNECT, y no siempre notarás esto sin una verificación explícita de response.http_version.
HTTP/2 proporciona una ganancia real en tres escenarios: cuando haces muchas solicitudes a un mismo dominio (el multiplexado en una sola conexión reduce las latencias), cuando es importante reducir la visibilidad de las solicitudes para la protección anti-bots (menos handshakes TCP desde una IP se ve más natural), y cuando el sitio objetivo exige HTTP/2 y devuelve errores o contenido recortado a los clientes en HTTP/1.1 — esto a veces ocurre en sitios en Cloudflare.
Para tareas donde la velocidad y estabilidad de las conexiones HTTP/2 a través de proxies son importantes, son más adecuados los proxies de centros de datos dedicados — son más rápidos que los residenciales y generalmente soportan correctamente el tunneling de protocolos modernos.
5 errores típicos con proxies en httpx
1. Recrear el cliente en cada solicitud
Código del tipo async with httpx.AsyncClient(...) as client: await client.get(url) dentro de un bucle — esto es un antipatrón. Cada creación de cliente implica una nueva conexión TCP, un nuevo handshake TLS, ignorando completamente el pooling de conexiones. En 1000 solicitudes, la diferencia de velocidad entre el enfoque correcto e incorrecto puede alcanzar de 5 a 10 veces.
2. Falta de timeouts o timeouts demasiado grandes por defecto
Por defecto, httpx establece un timeout de 5 segundos para todo de una vez — connect, read, write, pool. Al trabajar a través de proxies inestables, esto es poco para connect, pero demasiado para read, si el proxy simplemente "se cuelga". Divide los timeouts explícitamente:
timeout = httpx.Timeout(
connect=5.0,
read=10.0,
write=10.0,
pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)
3. Ignorar los límites del pool de conexiones
Con un gran número de tareas paralelas (gather en 500+ corutinas sin semáforo), httpx comienza a lanzar PoolTimeout, porque el límite de conexiones en el pool (por defecto 100) se ha agotado. Ajusta los límites explícitamente a través de httpx.Limits(max_connections=200, max_keepalive_connections=50) y siempre utiliza un semáforo para controlar el paralelismo.
4. Desactivar la verificación sin entender las consecuencias
Muchos copian verify=False de Stack Overflow para deshacerse de errores SSL al trabajar a través de proxies con certificados MITM. Esto abre la puerta a ataques de "hombre en el medio" y puede llevar a fugas de datos a través de un proxy comprometido. Si el proxy requiere un certificado especial, añádelo explícitamente a través de verify="/path/to/cert.pem", en lugar de desactivar la verificación por completo.
5. Clientes no cerrados y fuga de descriptores de archivos
Si creas un pool de clientes manualmente (como en el ejemplo anterior), no olvides llamar a await client.aclose() para cada uno de ellos al finalizar. Los scripts que funcionan durante mucho tiempo sin un cierre explícito de conexiones alcanzan el límite de descriptores de archivos abiertos del sistema operativo (generalmente 1024 en Linux por defecto), y el parser falla con un error misterioso Too many open files después de unas horas de funcionamiento.
Qué tipo de proxy elegir para el scraping
La elección del proxy influye directamente en cuántos errores de la lista anterior verás — proxies inestables provocan timeouts y desconexiones, incluso si el código está escrito a la perfección.
| Tipo de proxy | Velocidad | Riesgo de bloqueo | Cuándo usar |
|---|---|---|---|
| Centro de datos | Alta | Alto en sitios protegidos | API abierta, protección anti-bots débil |
| Residenciales | Media | Bajo | Wildberries, Ozon, redes sociales |
| Móviles | Media-baja | Mínimo | Instagram, TikTok API, protección estricta |
Para scraping asíncrono con alta velocidad de solicitudes a APIs con protección moderada, los proxies de centros de datos siguen siendo una opción económica — lo principal es distribuir uniformemente la carga en el pool, como se muestra en el código anterior.
Lista de verificación antes de la producción
- Los clientes httpx se crean una vez y se reutilizan, no se crean para cada solicitud
- Los timeouts se dividen en connect/read/write/pool, no se utiliza el valor por defecto
- El paralelismo se limita a través de
asyncio.Semaphore - Los límites del pool de conexiones se ajustan a la carga real
- Hay lógica de reintento con manejo de ProxyError, ConnectTimeout, ReadTimeout
- Todos los clientes se cierran explícitamente a través de
aclose()al finalizar - verify=False no se utiliza sin necesidad extrema
- HTTP/2 se habilita de manera consciente y se verifica a través de response.http_version
Conclusión
httpx proporciona una poderosa herramienta para trabajar asíncronamente con proxies y HTTP/2, pero requiere una cuidadosa configuración del pool de clientes, timeouts y límites de conexiones. La mayoría de las caídas de parsers en producción están relacionadas no con la biblioteca, sino con errores arquitectónicos: recreación de clientes, falta de limitación del paralelismo y manejo incorrecto de excepciones de proxies.
Si estás construyendo un parser para marketplaces o APIs de redes sociales que monitorean activamente la actividad sospechosa, la estabilidad de la infraestructura de proxies es tan importante como el código. Para tales tareas, vale la pena considerar los proxies residenciales — reducen la proporción de errores ProxyError y ConnectTimeout debido a la mayor reputación de las direcciones IP en comparación con los baratos pools de centros de datos.