Introducción: Dos librerías, distinto funcionamiento en AWS Lambda
En el ecosistema de Python, requests es la opción por defecto para muchos cuando interactúan con HTTP. Bajo el lema «HTTP for Humans» («HTTP para humanos»), requests ofrece una API intuitiva y de alto nivel que abstrae los mecanismos de cabeceras, parámetros de consulta, cookies y serialización JSON.
Cualquier desarrollador de Python usa por defecto requests. Es elegante, intuitiva y prácticamente un sinónimo de HTTP en Python. Por eso, al crear una función en Python que llama a un webhook externo o a un microservicio interno, la reacción inmediata es:
pip install requests
Sin embargo, bajo esa superficie tan amigable para el desarrollador, requests en realidad no gestiona las comunicaciones de red de bajo nivel por sí misma. En su lugar, se apoya en urllib3 como motor de transporte subyacente.
Qué hace realmente cada librería
Para entender por qué deberías prescindir de requests en Lambda, primero es necesario comprender la relación entre ambos paquetes:
-
urllib3es un cliente de transporte HTTP de bajo nivel. Gestiona directamente las conexiones de sockets, el pooling de conexiones, la resolución DNS y los handshakes SSL/TLS. -
requestses una capa envolvente de alto nivel, construida sobreurllib3.
requests no reinventa la rueda; delega el trabajo pesado en urllib3. Lo que requests aporta es mejoras de sintaxis: codificación automática de diccionarios, gestión de cookies, serialización JSON integrada y objetos de respuesta de alto nivel.
Ambas librerías realizan un trabajo similar pero los entornos de ejecución (runtimes) de AWS Lambda ya incluyen preinstalados boto3 y botocore, los cuales dependen directamente de urllib3. Como resultado, urllib3 ya está disponible de forma nativa en cualquier entorno Python de AWS Lambda sin añadir un solo kilobyte al tamaño de tu paquete de despliegue.
Actualizaciones del entorno de ejecución: ¿acaso no puede hacerlo otro?
Una de las mayores ventajas operativas de AWS Lambda es el ciclo de vida gestionado de su entorno de ejecución (Managed Runtime Lifecycle); como nos enseñó el episodio Trash of the Titans, siempre es mejor cuando alguien más lo hace por ti.
Cómo gestiona AWS urllib3
Los runtimes de Python en AWS Lambda vienen con el SDK de AWS (boto3 / botocore) preinstalado. Dado que botocore depende de urllib3, urllib3 ya está presente dentro de tu entorno Lambda sin necesidad de empaquetarlo.
Además, AWS aplica automáticamente parches de seguridad, correcciones de errores menores y actualizaciones de mantenimiento en los runtimes gestionados mediante despliegues de runtime en dos fases. Cuando se corrige una vulnerabilidad en urllib3, AWS actualiza el entorno subyacente en toda tu flota por ti, sin necesidad de un nuevo despliegue ni de ejecutar tus pipelines de CI/CD.
Qué ocurre cuando empaquetas requests
requests no forma parte del runtime nativo de Lambda. Cuando la incluyes en tu archivo zip o en una Lambda Layer, se instalan todas sus dependencias:
requests==2.34.2
┣━━ ✓ certifi required: >=2023.5.7 installed: 2026.6.17
┣━━ ✓ charset-normalizer required: >=2,<4 installed: 3.4.7
┣━━ ✓ idna required: >=2.5,<4 installed: 3.18
┗━━ ✓ urllib3 required: >=1.26,<3 installed: 2.6.3
Este subárbol de dependencias no está gestionado por AWS, y tú eres el responsable de mantenerlo al día, con las siguientes consecuencias:
-
Tú eres dueño del ciclo de mantenimiento: AWS nunca actualizará ni parchará
requestspor ti. Si se descubre una vulnerabilidad, todas y cada una de las funciones Lambda que incluyanrequestsseguirán expuestas hasta que alguien actualice el archivorequirements.txt, recompile el artefacto y lo vuelva a desplegar. - Exposición a vulnerabilidades de seguridad: Los escáneres de seguridad detectan dependencias desactualizadas en CI/CD. Por ejemplo, vulnerabilidades recientes como CVE-2026-25645 (extracción en rutas temporales predecibles). Sé que sabes que esto probablemente no afecte a un entorno aislado como Lambda, pero las herramientas de seguridad estática (como Amazon Inspector) generarán una alerta automáticamente. Buena suerte explicando a tu equipo de seguridad que un CVE específico es un «falso positivo».
-
Aumento de dependencias: Empaquetar
requests,certifi,idnaycharset-normalizerengorda los paquetes de despliegue, incrementando los tiempos de subida y prolongando ligeramente la descompresión del código en los inicios en frío (cold starts). Además, estas dependencias adicionales pueden introducir problemas de seguridad imprevistos, como los incidentes recientes con los paquetes de certifi y los certificados de eMudhra.
Al cambiar a la versión nativa de urllib3 del runtime, eliminas todo un subárbol de dependencias externas y delegas la aplicación de parches de seguridad en AWS.
Entendiendo la fase INIT de Lambda
En AWS Lambda, la ejecución se divide en dos fases bien diferenciadas del ciclo de vida:
-
La fase de inicialización (INIT): Ejecuta todo el código a nivel de módulo que está fuera de
lambda_handler. Esto ocurre una sola vez cuando se arranca un entorno de ejecución. -
La fase de invocación (Invocation): Ejecuta
lambda_handler(event, context)en cada petición recibida.
Un antipatrón clásico con requests es invocar requests.get() directamente dentro del handler:
def lambda_handler(event, context):
# ¡Crea una conexión completamente nueva en cada invocación!
response = requests.get("https://api.example.com/data")
return response.json()
Dado que requests.get() no mantiene estado, cada invocación realiza una nueva resolución DNS, handshake TCP y negociación TLS desde cero. Bajo un alto volumen de tráfico, esto añade algun milisegundo de latencia a cada llamada.
La solución: urllib3.PoolManager global
urllib3 se articula en torno a PoolManager, que es seguro para hilos (thread-safe) y mantiene los sockets abiertos entre invocaciones cuando se declara en el ámbito estático de INIT:
import urllib3
# Inicializado UNA SOLA VEZ durante la fase INIT de Lambda
http = urllib3.PoolManager(maxsize=10)
def lambda_handler(event, context):
# Reutiliza la conexión TCP/TLS existente en invocaciones "warm"
response = http.request("GET", "https://api.example.com/data")
...
Como el entorno de ejecución se mantiene activo (warm) entre invocaciones, la sesión TLS permanece establecida, reduciendo la sobrecarga de red a prácticamente cero en las llamadas subsiguientes.
Se puede obtener un resultado similar creando un objeto de sesión con la librería requests:
import requests
# Inicializado UNA SOLA VEZ durante la fase INIT de Lambda
session = requests.Session()
def lambda_handler(event, context):
# Reutiliza la conexión TCP/TLS existente en invocaciones "warm"
response = session.get("https://api.example.com/data")
...
Benchmarks de rendimiento y Cold-Start
Para comprobar el impacto en el rendimiento, se probaron 2 funciones Lambda diferentes utilizando aws lambda power tuning con los mismos parámetros:
{
"lambdaARN": "lambda_arn",
"powerValues": [128],
"num": 100,
"payload": {}
}
| Métrica |
requests (Handler) |
urllib3.PoolManager (INIT) |
|---|---|---|
| Tamaño del paquete | 653 KB | 582 bytes |
| Inicio en frío (Cold Start) | 840 ms | 915 ms |
| Invocación promedio | 340 ms | 72 ms |
| Consumo de memoria | 63 MB | 56 MB |
Guía de migración: de requests a urllib3
La migración es muy sencilla; cualquier LLM moderno puede hacerla por ti, solo debes indicarle que utilice PoolManager.
Aquí tienes los reemplazos directos para los patrones más comunes:
Patrón 1: Petición GET simple con parámetros de consulta
-
Antes (
requests):
import requests
params = {"status": "active", "limit": 20}
resp = requests.get("https://api.example.com/items", params=params, timeout=5)
data = resp.json()
-
Después (
urllib3):
import json
import urllib3
http = urllib3.PoolManager()
params = {"status": "active", "limit": 20}
resp = http.request(
"GET", "https://api.example.com/items", fields=params, timeout=5.0
)
data = json.loads(resp.data.decode("utf-8"))
Patrón 2: POST con carga útil (payload) JSON y cabeceras personalizadas
-
Antes (
requests):
import requests
headers = {"Authorization": "Bearer secret-token"}
payload = {"name": "Widget", "price": 49.99}
resp = requests.post(
"https://api.example.com/products", json=payload, headers=headers
)
-
Después (
urllib3):
import json
import urllib3
http = urllib3.PoolManager()
headers = {
"Authorization": "Bearer secret-token",
"Content-Type": "application/json",
}
payload = {"name": "Widget", "price": 49.99}
resp = http.request(
"POST",
"https://api.example.com/products",
body=json.dumps(payload).encode("utf-8"),
headers=headers,
)
Patrón 3: Configurar reintentos automáticos con backoff exponencial
En requests, configurar reintentos requiere importar HTTPAdapter y Retry y montarlos en una sesión personalizada. En urllib3, los reintentos son una funcionalidad nativa de primer nivel:
import urllib3
from urllib3.util import Retry
# Configurar la estrategia de backoff
retries = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504],
raise_on_status=False,
)
# Asociar los reintentos globalmente al pool manager
http = urllib3.PoolManager(retries=retries)
def lambda_handler(event, context):
response = http.request("GET", "https://api.example.com/reliable-endpoint")
return {"status": response.status}
Resumen: Por qué deberías dejar de usar requests en tus Lambdas
Pasar de requests a urllib3 en tus Lambdas le aporta beneficios inmediatos a tu equipo de ingeniería:
- Cero dependencias de terceros que empaquetar, comprimir en zip o compilar en capas (layers).
- Parches automáticos de vulnerabilidades mantenidos directamente por AWS.
- Invocaciones promedio más rápidas y ejecuciones «warm» ultrarrápidas gracias a pools de conexiones persistentes.
- Informes de auditoría limpios sin avisos de CVE transitivos.
Tu código se mantiene limpio, tus Lambdas se ejecutan más rápido y tu CTO no tendrá que preocuparse por el próximo zero-day en una dependencia.
Top comments (1)
The benchmark numbers are wild—invocation time dropping from 340ms to 72ms just by moving to urllib3.PoolManager is a massive win. But I'd add one important nuance to the "requests vs urllib3" debate:
requests.Session() does exactly the same connection pooling as urllib3.PoolManager if you instantiate it in the INIT phase. So the 72ms isn't because urllib3 is faster—it's because the article compares a pooled urllib3 client against a completely unpooled requests.get() call. If you just declare session = requests.Session() globally, you get the same performance boost (and keep the nice API).
That said, the dependency argument is 100% valid. Dropping requests, certifi, and charset-normalizer to rely on the AWS-managed urllib3 is a huge win for both cold starts and security patching. Great write-up!