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ística | Requests | HTTPX | AIOHTTP |
|---|---|---|---|
| Uso síncrono | Sí | Sí | No |
| Uso asíncrono | No | Sí | Sí |
| HTTP/2 | No | Sí (con paquete adicional) | No |
| Proxy (HTTP/HTTPS) | Nativo | Nativo | Nativo |
| Proxy SOCKS | Con requests[socks] | Con httpx[socks] | Con paquete externo |
| Tiempo de espera por defecto | Ninguno (espera indefinidamente) | 5 segundos | 5 minutos (total) |
| Seguir redirecciones | Activado por defecto | Desactivado por defecto | Activado por defecto |
| Reutilización de conexiones | Con Session | Con Client | Con ClientSession |
| Curva de aprendizaje | Muy fácil | Fácil | Media |
| Trabajo más adecuado | Scripts simples, poco volumen | Proyectos mixtos síncronos y asíncronos | Alta 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:
pip install requestsUna petición a través de un proxy:
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:
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:
pip install httpxEl uso síncrono es casi idéntico al de Requests:
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:
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:
pip install aiohttpimport 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:
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 conraise_for_status=Trueal abrir la sesión.
Estos son los códigos que verás con más frecuencia al trabajar con proxies:
| Código | Significado | ¿Qué hacer? |
|---|---|---|
| 407 | Falló la autenticación del proxy | Revisa el usuario, la contraseña y la codificación de caracteres especiales |
| 429 | El sitio de destino aplica un límite de velocidad | Reduce la concurrencia, espera y reintenta, reparte más las IP |
| 403 | El sitio de destino rechaza la petición | Revisa el User-Agent y el tipo de IP |
| 502 / 504 | El proxy no pudo llegar al destino | Reintenta 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:
| Requests | HTTPX | Nota |
|---|---|---|
requests.get(url) | httpx.get(url) | Igual |
proxies={"http": p, "https": p} | proxy=p | Un solo parámetro |
timeout=None por defecto | timeout=5 por defecto | En HTTPX el tiempo de espera viene activado |
| Sigue redirecciones por defecto | Requiere follow_redirects=True | HTTPX 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.




