HTTPX, Requests y AIOHTTP: comparativa

Publicado:

12 min de lectura

Acar Diveroli
Autor: Acar Diveroli
Bloques de código en tres columnas

Para enviar peticiones HTTP en Python destacan tres bibliotecas: Requests, HTTPX y AIOHTTP. Las tres hacen el mismo trabajo básico, pero la diferencia entre ellas no aparece al escribir un script pequeño, sino cuando tienes que gestionar miles de peticiones a la vez. En este artículo comparamos las tres bibliotecas en facilidad de uso, concurrencia, soporte de proxy, gestión de errores y ecosistema, damos ejemplos que funcionan para cada una y mostramos el camino para pasar de Requests a código asíncrono.

El código de los ejemplos se probó con Python 3.13 y las versiones Requests 2.32, HTTPX 0.28 y AIOHTTP 3.13.

Las tres bibliotecas en pocas palabras

  • Requests: la biblioteca HTTP más extendida de Python. Funciona de forma síncrona, es muy fácil de aprender y encontrarás documentación y ejemplos en todas partes. Recuerda que el programa espera en esa línea hasta que termina cada petición.
  • HTTPX: ofrece una interfaz muy parecida a la de Requests, tanto síncrona como asíncrona. También soporta HTTP/2. Suele ser la forma menos costosa de llevar código existente de Requests a un modelo asíncrono.
  • AIOHTTP: una biblioteca diseñada como asíncrona desde el principio. Además del cliente incluye un servidor web. Es una opción madura y muy utilizada para trabajos que necesitan una concurrencia muy alta.

¿Qué significan síncrono y asíncrono?

Como este concepto está en el centro de la comparación, lo explicamos brevemente. Un cliente síncrono envía la petición y espera hasta que llega la respuesta; mientras tanto, el programa no hace nada más. Un cliente asíncrono, en cambio, cede el control al bucle de eventos después de enviar la petición; mientras espera la respuesta, otras peticiones pueden salir.

La mayor parte del tiempo de una petición web transcurre en la red, es decir, esperando a que responda el servidor de destino. En código síncrono esa espera se desperdicia; en código asíncrono, en el mismo tiempo se pueden esperar decenas de peticiones más. La diferencia no se nota al descargar cien páginas; al descargar diez mil, se convierte en la diferencia entre horas y minutos.

El precio de esta ventaja es la complejidad: la sintaxis async/await, el bucle de eventos y la incompatibilidad con bibliotecas que no son asíncronas. Por eso es un error decir que «lo asíncrono siempre es mejor»; si no lo necesitas, el código síncrono es más legible y más fácil de mantener.

Las diferencias principales en una tabla

CaracterísticaRequestsHTTPXAIOHTTP
Uso síncronoNo
Uso asíncronoNo
HTTP/2NoSí (con paquete adicional)No
Proxy (HTTP/HTTPS)NativoNativoNativo
Proxy SOCKSCon requests[socks]Con httpx[socks]Con paquete externo
Tiempo de espera por defectoNinguno (espera indefinidamente)5 segundos5 minutos (total)
Seguir redireccionesActivado por defectoDesactivado por defectoActivado por defecto
Reutilización de conexionesCon SessionCon ClientCon ClientSession
Curva de aprendizajeMuy fácilFácilMedia
Trabajo más adecuadoScripts simples, poco volumenProyectos mixtos síncronos y asíncronosAlta concurrencia

La fila del tiempo de espera por defecto es la diferencia que más se pasa por alto entre las tres bibliotecas. Si en Requests no indicas un tiempo de espera, la petición puede quedarse esperando para siempre; por eso añadir timeout= a cada llamada de Requests debería ser un hábito.

Requests: simple y síncrono

Instalación:

bash
pip install requests

Una petición a través de un proxy:

python
import requests

PROXY = "http://user:pass@pr.proxynet.io:8000"
proxies = {"http": PROXY, "https": PROXY}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=20)
print(response.status_code, response.json())

La clave https del diccionario proxies es el proxy que se usa al ir a direcciones HTTPS. Que el valor empiece por http:// es correcto: el tráfico HTTPS se tuneliza dentro de la conexión establecida con el proxy.

Si vas a enviar varias peticiones, usa Session: las conexiones se reutilizan, las cookies se conservan y el proxy se configura una sola vez:

python
import requests

PROXY = "http://user:pass@pr.proxynet.io:8000"

with requests.Session() as session:
    session.proxies.update({"http": PROXY, "https": PROXY})
    session.headers.update({"User-Agent": "data-collector-bot/1.0"})
    for path in ["ip", "headers", "user-agent"]:
        r = session.get(f"https://httpbin.org/{path}", timeout=20)
        print(path, r.status_code)

El límite de Requests es la concurrencia. Si descargas 1.000 páginas una tras otra, el tiempo total se acerca a la suma del tiempo de cada petición. Es posible aliviarlo con concurrent.futures.ThreadPoolExecutor, pero con volúmenes muy altos una biblioteca asíncrona trabaja con más eficiencia.

HTTPX: entre los dos mundos

Instalación:

bash
pip install httpx

El uso síncrono es casi idéntico al de Requests:

python
import httpx

PROXY = "http://user:pass@pr.proxynet.io:8000"

with httpx.Client(proxy=PROXY, timeout=20) as client:
    response = client.get("https://httpbin.org/ip")
    print(response.json())

La versión asíncrona del mismo código envía varias peticiones a la vez:

python
import asyncio
import httpx

PROXY = "http://user:pass@pr.proxynet.io:8000"
URLS = ["https://httpbin.org/ip"] * 10

async def main():
    async with httpx.AsyncClient(proxy=PROXY, timeout=20) as client:
        responses = await asyncio.gather(*(client.get(url) for url in URLS))
        print([r.status_code for r in responses])

asyncio.run(main())

Aquí las 10 peticiones no salen una tras otra, sino al mismo tiempo. El tiempo total se acerca al de la petición más lenta.

Para usar HTTP/2 basta con instalar pip install "httpx[http2]" y pasar http2=True al cliente. Cuando envías muchas peticiones al mismo servidor, la multiplexación sobre una sola conexión reduce el costo de abrir conexiones. Con un proxy, HTTP/2 funciona dentro del túnel CONNECT, entre tú y el sitio de destino; el proxy no necesita soportar HTTP/2.

AIOHTTP: para alta concurrencia

Instalación:

bash
pip install aiohttp
python
import asyncio
import aiohttp

PROXY = "http://pr.proxynet.io:8000"
AUTH = aiohttp.BasicAuth("user", "pass")
URLS = ["https://httpbin.org/ip"] * 10

async def fetch(session, url):
    async with session.get(url, proxy=PROXY, proxy_auth=AUTH) as response:
        return response.status

async def main():
    async with aiohttp.ClientSession() as session:
        statuses = await asyncio.gather(*(fetch(session, url) for url in URLS))
        print(statuses)

asyncio.run(main())

En AIOHTTP el proxy no se define en la sesión, sino en cada petición con el parámetro proxy=. Puedes pasar las credenciales por separado con proxy_auth o escribirlas dentro de la dirección (http://user:pass@...). Con este diseño resulta natural usar un proxy distinto en cada petición; si quieres rotar tu propia lista de IP, AIOHTTP lo pone fácil.

Otra diferencia de AIOHTTP es que el cuerpo de la respuesta debe leerse dentro del gestor de contexto. Llamar a await response.text() después de salir del bloque async with da error; lee el cuerpo dentro del bloque y guárdalo en una variable.

Limitar la concurrencia

Al trabajar de forma asíncrona, no dejes la concurrencia sin límite. Lanzar diez mil URL de golpe con asyncio.gather fuerza el límite de descriptores de archivo de tu propio sistema y, además, lleva al sitio de destino a aplicar un límite de velocidad de inmediato. Limitar con asyncio.Semaphore el número de peticiones abiertas a la vez protege a ambas partes:

python
import asyncio
import httpx

PROXY = "http://user:pass@pr.proxynet.io:8000"
URLS = [f"https://httpbin.org/get?i={i}" for i in range(100)]
LIMIT = asyncio.Semaphore(10)

async def fetch(client, url):
    async with LIMIT:
        r = await client.get(url)
        return r.status_code

async def main():
    async with httpx.AsyncClient(proxy=PROXY, timeout=20) as client:
        results = await asyncio.gather(*(fetch(client, u) for u in URLS))
        print(sum(1 for s in results if s == 200), "correctas")

asyncio.run(main())

En este ejemplo se ponen en cola cien peticiones, pero como mucho diez están abiertas al mismo tiempo. Ajusta el límite a la tolerancia del sitio de destino y al número de conexiones simultáneas que permite tu plan de proxy.

Gestión de errores y reintentos

En las tres bibliotecas los errores de red llegan como excepciones; sin embargo, los códigos de error HTTP (404, 429, 500) no son excepciones por defecto. Comprobar la respuesta es tarea tuya:

  • En Requests y HTTPX, la llamada response.raise_for_status() lanza una excepción con las respuestas 4xx y 5xx.
  • En AIOHTTP se consigue lo mismo con response.raise_for_status() o con raise_for_status=True al abrir la sesión.

Estos son los códigos que verás con más frecuencia al trabajar con proxies:

CódigoSignificado¿Qué hacer?
407Falló la autenticación del proxyRevisa el usuario, la contraseña y la codificación de caracteres especiales
429El sitio de destino aplica un límite de velocidadReduce la concurrencia, espera y reintenta, reparte más las IP
403El sitio de destino rechaza la peticiónRevisa el User-Agent y el tipo de IP
502 / 504El proxy no pudo llegar al destinoReintenta con otra IP de salida

Escribe la lógica de reintentos con espera exponencial: un segundo en el primer intento, luego dos, luego cuatro. Insistir a intervalos fijos puede terminar en el bloqueo total de una IP que ya recibió un 429.

Rendimiento: ¿cuál es más rápida?

Al elegir una biblioteca, la pregunta «¿cuál es más rápida?» suele mirar en el lugar equivocado. La mayor parte del tiempo de una petición web transcurre en la red; el tiempo de procesamiento de la propia biblioteca es pequeño en comparación.

Lo decisivo es cuántas peticiones puedes esperar al mismo tiempo:

  • Un cliente síncrono espera cada petición por turno.
  • Un cliente asíncrono puede esperar cientos de peticiones a la vez.

Es decir, para un trabajo de 10 páginas Requests es suficiente. Para uno de 10.000 páginas, el cliente asíncrono de HTTPX o AIOHTTP acorta el tiempo de forma considerable. Dentro de la misma categoría (entre HTTPX asíncrono y AIOHTTP) hay diferencias medibles y AIOHTTP suele ir por delante en rendimiento bruto; aun así, en la mayoría de los proyectos esa diferencia queda eclipsada por el tiempo de respuesta del sitio de destino y la latencia del proxy.

¿Cómo combinar proxy y concurrencia?

Las bibliotecas asíncronas pueden enviar muchas peticiones a la vez; pero si todas salen de una sola IP, el sitio de destino no tarda en aplicar un límite de velocidad. A medida que aumentas la concurrencia, también tienes que pensar en cómo repartir las IP:

  • Una IP distinta en cada petición: con Proxies rotativos usas una sola dirección de entrada y obtienes una IP de salida distinta en cada petición. No hace falta cambiar el código; todos los ejemplos anteriores funcionan tal cual.
  • La misma IP durante toda la sesión: en flujos con estado, como un inicio de sesión o un carrito, Proxies de sesión fija mantiene la misma IP durante un tiempo determinado.
  • Rotar tu propia lista: si tienes una lista fija de IP, puedes hacer la rotación en el código; tienes un ejemplo paso a paso en nuestro artículo ¿Cómo rotar proxies en Python?.
  • Tipo de IP: en destinos protegidos, el tipo de IP pesa más que la concurrencia; explicamos por qué en Proxies residenciales y de centro de datos: diferencias.

Pasar de Requests a HTTPX

Si quieres llevar un proyecto existente de Requests a un modelo asíncrono, HTTPX es el camino más corto. Estas son las diferencias a tener en cuenta en la migración:

RequestsHTTPXNota
requests.get(url)httpx.get(url)Igual
proxies={"http": p, "https": p}proxy=pUn solo parámetro
timeout=None por defectotimeout=5 por defectoEn HTTPX el tiempo de espera viene activado
Sigue redirecciones por defectoRequiere follow_redirects=TrueHTTPX no las sigue por defecto
Session()Client() / AsyncClient()Misma lógica
response.json()response.json()Igual

La mayor parte del código funciona sin cambios; las diferencias se concentran en las cinco filas de arriba. Migrar primero con el Client síncrono y ejecutar las pruebas, y solo después pasar a AsyncClient, reduce el riesgo.

¿Cuál deberías elegir?

  • Elige Requests para scripts pequeños, tareas de automatización, integraciones con API y trabajos en los que la concurrencia no importa.
  • Elige HTTPX si hoy empiezas en síncrono y es probable que mañana pases a asíncrono, si necesitas HTTP/2 o si quieres los dos modos en el mismo proyecto.
  • Elige AIOHTTP para trabajos de recogida de datos de volumen muy alto diseñados como asíncronos desde el principio, y cuando la misma aplicación necesita cliente y servidor.

Si con descargar las páginas por HTTP no basta, es decir, si el contenido se carga con JavaScript, estas bibliotecas por sí solas no son suficientes y necesitas automatizar un navegador. Tratamos esta distinción en Web scraping: ¿JavaScript o Python?. Para la automatización de navegadores en Python, consulta nuestras guías sobre Selenium y SeleniumBase con proxy.

Preguntas frecuentes

¿Es difícil pasar de Requests a HTTPX?

Normalmente no. Escribir httpx.get en lugar de requests.get funciona directamente en la mayoría del código sencillo. Las diferencias están en detalles como que el tiempo de espera viene activado por defecto, que las redirecciones no se siguen por defecto y el nombre del parámetro del proxy. La tabla de migración de arriba recoge estas diferencias.

¿Cómo se usa un proxy SOCKS5?

Instala pip install "requests[socks]" para Requests o pip install "httpx[socks]" para HTTPX y usa el esquema socks5:// en la dirección del proxy. Si quieres que la resolución DNS también se haga en el lado del proxy, escribe socks5h:// en Requests. Para las diferencias entre protocolos, consulta nuestro artículo Proxy SOCKS o HTTP: cuál elegir.

¿El código asíncrono siempre es mejor?

No. El código asíncrono es más complejo y sus errores son más difíciles de depurar. Para un trabajo que no se beneficia de verdad de la concurrencia, el código síncrono es más legible y más fácil de mantener.

¿Usar hilos con Requests es una alternativa a lo asíncrono?

Para volúmenes medios, sí. Con ThreadPoolExecutor puedes ejecutar decenas de peticiones en paralelo y el código sigue siendo síncrono. Con cientos de peticiones simultáneas aumenta el costo de memoria de los hilos; a partir de ahí un cliente asíncrono es más eficiente.

¿Tengo que escribir las credenciales del proxy en el código?

No. Las tres bibliotecas leen las variables de entorno HTTP_PROXY y HTTPS_PROXY (en HTTPX trust_env está activado por defecto). Guardar las credenciales en una variable de entorno reduce el riesgo de filtrarlas al subir el código a un repositorio.

¿Cuántas peticiones debería abrir a la vez?

No hay un número correcto único. Lo determinan la tolerancia del sitio de destino, las conexiones simultáneas que permite tu plan de proxy y los límites de tu propia máquina. Empezar con diez y subir mientras no aparezcan respuestas 429 es un método seguro.

En resumen

Las tres bibliotecas hacen bien su trabajo; la elección depende de la escala del proyecto. Requests destaca en trabajos sencillos, HTTPX en flexibilidad y preparación para el futuro, y AIOHTTP en alta concurrencia. Elijas la que elijas, define un tiempo de espera, limita la concurrencia y escribe los reintentos con espera exponencial. A medida que crece el volumen, la estrategia de IP importa tanto como la biblioteca. Para una infraestructura de recogida de datos a gran escala, consulta nuestras soluciones de extracción de datos.