Introducción
AWS anunció Lambda Web Functions, una forma de ejecutar aplicaciones web y APIs sobre Lambda ejecutando directamente un servidor HTTP de Node.js, sin adaptadores ni handlers por evento. Aquí te describo mi propio análisis.
Este artículo no pretende ser una guía de cómo usar Lambda Web Functions. Hago un recorrido corto por las características que importan, referencio blogs de colegas y muestro mi propio enfoque de cuándo utilizarla y cuando No.
Para no quedarme en la teoría, armé un pequeño playground con ejemplos: desde un agente de IA con streaming hasta microservicios simulando alto tráfico. Además, hice algunos experimentos de concurrencia y comportamiento de escalado.
Qué es una Lambda Web Function
Una Lambda Web Function es cómputo serverless que corre un servidor HTTP normal. Tu aplicación escucha en el host y puerto que Lambda te pasa en la variable AWS_LAMBDA_HTTP_ENDPOINT, exactamente el mismo código que correrías con node index.js en tu máquina. AWS te da el endpoint HTTPS, el TLS y el escalado; de tu parte toca escribir Express (o el framework que uses) sin adaptadores.
NO es un reemplazo universal de la Lambda de siempre que devuelve un event handler. Esta sigue siendo la opción natural para lo que es dirigido por eventos: procesar mensajes de una SQS, reaccionar a un evento de S3, consumir un stream. Tampoco es un servidor que tengas "siempre encendido": escala a cero cuando no hay tráfico. Y por ahora solo soporta runtime Node.js.
Lo que cambia de verdad
Muchos de los protocolos que vas a ver en las demos (REST, SSE, streaming, webhooks) ya se podían hacer antes de otra forma. Lo que cambia no son los protocolos, sino la forma de la Lambda:
- Modelo de programación. Antes escribías un handler que recibía un evento; ahora escribís un servidor HTTP común que escucha en un puerto.
- Concurrencia. En una Event Function, cada entorno atiende una petición a la vez. En una Web Function, un mismo entorno atiende varias en simultáneo, como un servidor Node común. Para algo que pasa casi todo el tiempo esperando (una base de datos, una API, un LLM), eso significa que muchas peticiones comparten un proceso en lugar de ocupar un entorno cada una.
- Parámetros importantes
| Parámetro | descripción |
|---|---|
maxConcurrencyPerEnvironment |
tope por entorno, 1–128, se configura en la revisión. El límite real puede ser menor, nunca mayor. Cambiarlo crea una nueva revisión |
maxEnvironments |
tope de entornos por región, se configura en el endpoint. |
rateLimit |
peticiones por segundo que acepta el endpoint, por región. Lo que supera rateLimit recibe 429. Lo mismo ocurre si no puede escalar más, pero mientras un entorno nuevo se inicializa, las requests esperan en cola. rateLimit se aplica por región, y sus valores deben ser múltiplos de 100 (hasta 1.000) o de 1.000 (por encima). |
maxEnvironments y rateLimit
|
se cambian en caliente con aws lambda-web update-web-function-endpoint, sin redeployar; en cambio maxConcurrencyPerEnvironment sí obliga a hacer una nueva revisión de la Lambda. |
-
Entrada y salida: HTTP crudo, no un payload. Una Event Function recibe un
event(un JSON ya parseado) y devuelve otro JSON; nunca tocás HTTP a mano. Una Web Function recibe y devuelve HTTP/1.1 crudo: Lambda reenvía a tu app el método, el path, la query string, el body y casi todos los headers, y tu servidor (Express, Fastify, Hono o el framework de Node que prefieras) ve unRequest/Responsenormal. Soporta GET, HEAD, POST, PUT, PATCH, OPTIONS y DELETE, solo HTTP/1.1 (un HTTP/1.0 lo rechaza con 505), y agrega headers de contexto comolambda-web-request-id. Es justo lo que te deja usar tu framework sin adaptador, pero el parseo y la validación del request quedan de tu lado. -
Seguridad: el endpoint es público, el modo de auth define quién valida. Todo endpoint es alcanzable desde internet, tenga el auth que tenga. Hay dos modos: con
IamAuth, Lambda valida la firma SigV4 y remueve el headerAuthorizationantes de reenviar (la autorización la hace la plataforma); conApplicationManaged, Lambda no autentica nada y reenvía todo a tu app, así que conocer la URL alcanza para llegar y es necesario implementar la autenticación. Sumá a eso que algunos headers no son de fiar (X-Forwarded-Hostllega sin validar; el cliente real está en el último valor deX-Forwarded-For) y que validar input, tamaño de request y abuso es responsabilidad de tu código o de un front door. - Facturación: Pagás CPU activa y memoria mientras el entorno exista. No se cobra tiempo de reloj. Una petición que espera a una base de datos o a una API externa consume poca CPU, y esa espera casi no se cobra.
- Proceso de larga vida. El entorno no se congela entre peticiones → hay estado global, conexiones reutilizadas y tareas en segundo plano.
⭐ Para el detalle de cada protocolo, el recorrido de edjgeek lo cubre muy bien, y el repositorios de ejemplos que acompaña ese blog muestra REST, SSE, NDJSON y webhooks funcionando.
Dónde NO lo usaría
Hay varios escenarios donde esta tipo de Lambda no aplica:
- Request/response simple y de bajo volumen. Un formulario que dispara un mail, o una Lambda que devuelve un JSON corto cada tanto. Ahí una Event Function con su function URL es más simple y suficiente; migrar no te da ningún beneficio.
- Trabajo dirigido por eventos. Procesar una cola SQS, reaccionar a un archivo en S3, consumir un stream. Eso es territorio de las Event Functions, y la propia documentación lo recomienda así.
- Tareas largas. El timeout de una petición llega hasta 900 s (15 min). Si tu proceso puede tardar más, no entra en una sola petición: hay que partirlo o usar otro mecanismo.
- WebSockets o HTTP/2. no mantiene conexiones bidireccionales persistentes de tipo TCP Upgrade abiertas directamente hacia el entorno de ejecución de la Lambda de forma continua. Si este es tu caso no se resuelve por aquí.
- En Tareas multi-paso elegimos Lambda durable functions. Pensadas para flujos de trabajo resilientes de varios pasos que pueden durar hasta un año, con checkpoints, reintentos y estados de espera que no consumen cómputo mientras aguardan. Casos de uso: procesar pagos, orquestar pedidos, flujos con aprobación humana o workflows de IA de varios pasos.
⭐ La regla práctica: si tu carga no es "un servidor HTTP que atiende tráfico concurrente", probablemente Lambda Web Functions no corresponda a tu caso de uso.
¿Reemplaza a API Gateway / function URLs?
Una Web Function ya trae su propio endpoint HTTPS. Pero reemplaza en los casos simples, no es un reemplazo directo.
Pensemos primero en las function URLs, la URL pública de una Lambda común. Desde el lado del cliente, para un request/response sencillo, casi no hay diferencia: hacés una petición HTTP y recibís una respuesta. Si lo que tenés es una Lambda con su URL devolviendo un JSON corto, migrar a Web Functions no te cambia nada visible. Donde empieza a tener sentido es cuando aparece concurrencia con mucha espera, streaming, o una app con framework y varias rutas.
Con API Gateway la cuestión es diferente, porque son de distinta naturaleza: una Web Function es cómputo con un endpoint incluido, y API Gateway es una puerta de entrada que se pone delante del cómputo.
Si tenés una app con su propio router (Express, Hono, Fastify), antes necesitabas API Gateway para exponerla; ahora el endpoint ya viene con HTTPS, autenticación y un límite global de peticiones. Para APIs internas o servicios entre equipos, eso suele alcanzar.
Pero API Gateway sigue teniendo sentido como front door. La propia documentación de buenas prácticas recomienda poner CloudFront o API Gateway delante del endpoint cuando necesitás dominio propio, inspección con WAF, caché o controles centralizados en el borde. Y hay features que API Gateway te da y la Web Function no: planes de uso y API keys por cliente, authorizers (JWT, Cognito), cuotas por consumidor. El límite de peticiones de la Web Function es un tope por endpoint y región, no por cliente.
⭐ Para una app o microservicio con su propio router, Lambda Web Functions te AHORRA una capa. Para una API pública con múltiples consumidores, cuotas y políticas en el borde, seguís necesitando API Gateway o CloudFront según el caso.
Casos de Uso del playground
Construí ejemplos sencillos que pueden servirte para llevar a la práctica rápidamente tus casos de uso.
📦 Aquí vas encontrar ejemplos con dos maneras de desplegar: manualmente por la CLi o via SAM : github.com/reinalau/lambda-web-functions-playground
Lambda Web Functions ya es compatible con IaC:
AWS CloudFormation ( ´AWS::Lambda::WebFunction resource´).
AWS CDK (L2 constructs available)
AWS SAM
Un agente strands harness con streaming
Es un agente de IA hecho con strands harness, que responde preguntas sobre el clima y ayuda a planificar viajes. Recibe un mensaje, llama a herramientas que consultan una API pública (Open-Meteo), razona sobre los datos y devuelve la respuesta al navegador token a token por SSE. Mantiene la conversación entre turnos, recuerda lo que ya le dijiste.
%%{init: {"theme": "base", "themeVariables": {"fontSize": "30px"}}}%%
flowchart LR
Client["Client / browser"] -->|"POST /chat"| Agent["weather-agent<br />Web Function (Express)<br />Strands harness"]
Agent -->|"SSE: stream tokens"| Client
Agent -->|"InvokeModelWithResponseStream"| Bedrock["Amazon Bedrock<br />Nova 2 Lite"]
Agent -->|"tools (no API key)"| OpenMeteo["Open-Meteo API<br />geocode + forecast"]
Agent -->|"SessionManager<br />by conversationId"| S3[("Amazon S3<br />session state")]
style Client fill:#2563eb,stroke:#1e40af,color:#ffffff
style Agent fill:#f59e0b,stroke:#b45309,color:#ffffff
style Bedrock fill:#15803d,stroke:#0f5027,color:#ffffff
style OpenMeteo fill:#64748b,stroke:#475569,color:#ffffff
style S3 fill:#4ade80,stroke:#16a34a,color:#063d1e
Por qué con Web Functions y no con otra cosa → Este caso pasa casi todo el tiempo esperando al modelo y a las herramientas, consumiendo muy poca CPU, y esa espera casi no se cobra. Además:
- El modelo de servidor HTTP me deja usar el harness de Strands y Express tal cual, sin reescribir nada como handler ni armar un adaptador.
- El streaming de tokens por SSE es nativo: devuelvo la respuesta a medida que se genera, sin esperar a tenerla completa.
- La concurrencia por entorno significa que muchas conversaciones conviven en el mismo proceso mientras esperan al LLM, en vez de ocupar un entorno entero cada una (como pasaría con una Event Function).
En una Event Function con function URL pagaría el tiempo de reloj completo de cada conversación y ocuparía un entorno por request. Acá no.
La implementación: Es Express corriendo como Web Function. El agente usa el harness de Strands (TypeScript) con builtinTools: [] para dejar solo las dos herramientas de clima (nada de shell ni acceso a archivos: menor superficie), sobre un modelo Amazon Nova en Bedrock.
El punto fino es el estado: como el entorno es efímero y no hay afinidad de sesión, la conversación no puede vivir en memoria; se persiste fuera, en S3, usando el SessionManager del SDK. Cada conversación se identifica por un id, y cualquier entorno puede retomarla. El detalle completo lo encontrás en el README del ejemplo.
Un BFF con microservicios (pedidos + inventario)
Este ejemplo consta de dos Lambda Web Functions que modelan un patrón BFF + microservicios, sin IA: solo API y DynamoDB. Un servicio orders (el BFF, público) recibe un pedido, reserva stock llamando a un servicio inventory (privado) y persiste el pedido. inventory expone el stock y lo reserva con un UpdateItem condicional que nunca lo deja en negativo (devuelve 409 si no alcanza).
Este caso es de un "backend de alto tráfico que espera mucho I/O": cada pedido llama a otro servicio y a una base de datos, y pasa la mayor parte del tiempo esperando esas dos cosas.
%%{init: {"theme": "base", "themeVariables": {"fontSize": "28px"}}}%%
flowchart LR
Client["Client"] -->|"POST /orders (no signing)"| Orders["orders BFF<br />Web Function<br />ApplicationManaged"]
Orders -->|"SigV4-signed call<br />service: lambda<br />x-amz-content-sha256"| Inventory["inventory<br />Web Function<br />IamAuth (private)"]
Orders -->|"PutItem"| OrdersDB[("DynamoDB<br />orders table")]
Inventory -->|"conditional UpdateItem<br />reserve stock or 409"| InventoryDB[("DynamoDB<br />inventory table")]
style Client fill:#22c55e,stroke:#15803d,color:#ffffff
style Orders fill:#f59e0b,stroke:#b45309,color:#ffffff
style Inventory fill:#f59e0b,stroke:#b45309,color:#ffffff
style OrdersDB fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
style InventoryDB fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
Algunas características:
- El servidor HTTP deja usar Express con su router tal cual;
ordersenruta por dentro y no necesito una función por ruta ni API Gateway delante. - La concurrencia por entorno deja que muchas peticiones convivan en el mismo proceso mientras esperan a
inventoryy a DynamoDB. - Cada servicio escala por separado: un pico en el BFF no obliga a sobredimensionar el inventario.
Comunicación entre servicios con IamAuth. La mayoría de las reseñas lo pasa por alto: inventory es privado (IamAuth), así que orders no lo invoca con un SDK → tiene que firmar la petición HTTP con SigV4 usando el servicio lambda, e incluir siempre la cabecera x-amz-content-sha256 (Lambda la usa como hash del payload en vez de leer el body).
El rol de orders necesita el permiso lambda:InvokeWebFunctionEndpoint sobre el endpoint de inventory, y Lambda elimina la cabecera Authorization antes de reenviar la request a la aplicación. Para endpoints multi-región la firma pasa a SigV4a.
La implementación: Dos servicios Express en TypeScript, cada uno su propia Web Function con su rol de mínimo privilegio. inventory guarda el stock en DynamoDB y hace la reserva atómica; orders firma su llamada a inventory, y si la reserva sale bien persiste el pedido en otra tabla. El detalle (despliegue ordenado, IAM y la receta de firma) está en el README del ejemplo.
Hice dos experimentos de caga sobre el BFF. Los números salen de los eventos de CloudWatch (invokeReport / initReport).
Experimento 1 ➡ Carga: ¿cuánta CPU se cobra?
Objetivo: analizar con tráfico concurrente qué fracción del tiempo del entorno se factura como CPU. 500 pedidos, 50 en paralelo (no 500 a la vez: 500 en total).
| Métrica | Valor |
|---|---|
Pedidos (todos 201) |
500 |
| Tiempo de reloj | 15 s |
| Entornos creados | 61 |
| Pico de concurrencia por entorno | 2 |
| CPU activa de serving (toda la flota) | ~49 s |
| CPU de arranque (toda la flota) | ~22 s |
| Vida de servicio sumada | ~24.461 s |
Análisis: la CPU de serving (~49 s) es ~0,2% de la vida de servicio sumada (~24.461 s). Cuidado con la lectura: ese 99,8% restante no es todo "espera de I/O". La carga duró 15 s, pero cada entorno siguió vivo ~400 s hasta escalar a cero, así que la mayor parte es ocio del entorno después de la carga, no espera por request. El punto importante, ese tiempo no se cobra como CPU (la memoria sí se cobra mientras el entorno exista). Medir la espera de I/O por request requiere los eventos requestComplete (nivel DEBUG), que no lo activé acá.
⭐ Un detalle de la concurrencia: 50 pedidos en paralelo deberían caber en pocos entornos (uno atiende hasta 64), pero se abrieron 61 con un pico de solo 2 por entorno. No es un parámetro que faltó setear como maxConcurrencyPerEnvironment porque solo fija el techo. Por debajo, Lambda usa un límite dinámico por entorno que ajusta según el uso de recursos, y lo baja cuando cada request consume CPU (acá, la firma SigV4). Con poca concurrencia por entorno, Lambda abre más entornos, y cada uno paga su arranque: eso explica el costo del Experimento 2. La ventaja de "menos entornos por concurrencia compartida" frente a una Event Function es real, pero el reparto lo decide la plataforma, no tu config.
Experimento 2 ➡ ¿Cuánto cuesta un pedido?
Objetivo: separar el costo de arranque del trabajo real por pedido. Dos corridas: 1 pedido en un entorno frío, y 200 pedidos secuenciales en el mismo entorno caliente.
| Corrida | Pedidos | CPU total |
|---|---|---|
| Entorno frío | 1 | ~780 ms |
| Entorno caliente | 200 | ~2.600 ms |
Análisis: el costo marginal por pedido es (2.600 − 780) / 199 ≈ 9 ms. O sea, un pedido en caliente cuesta ~9 ms de CPU (SigV4 + JSON + la parte de CPU del I/O), pero el primer pedido de un entorno frío cuesta ~780 ms (arranque de Node + carga del SDK + primera conexión), unas 85 veces más. En una Web Function, lo que domina el costo de CPU es cuántos entornos se abren, no cuántos pedidos se atienden.
Conclusiones
Lambda Web Functions no viene a reemplazar a la Lambda de siempre, suma una forma más, junto a las Event Functions y las durable functions. Su enfoque es claro: cargas que son, en esencia, un servidor HTTP atendiendo tráfico concurrente que pasa la mayor parte del tiempo esperando I/O. Ahí el modelo encaja muy bien.
Las ventajas que comprobé en el playground
- Código de servidor común (Express) sin adaptadores, que corre igual en local y en AWS.
- Concurrencia por entorno: cada entorno atiende varias peticiones mientras esperan I/O.
- Facturación por CPU activa: en la prueba del BFF, la CPU de serving fue ~0,2% de la vida de los entornos; la espera de I/O casi no se cobra.
- Streaming nativo (el SSE del agente) sin configuración extra.
- Controles en el endpoint (
rateLimit,maxEnvironments) para proteger a las dependencias que están detrás, como una base de datos. - Comunicación privada entre servicios con
IamAuth, aunque exige firmar las peticiones a mano con SigV4.
Sobre migrar
Si ya tenés una app Express o Hono corriendo en Lambda con un adaptador, la migración es mayormente quitar el adaptador y escuchar en AWS_LAMBDA_HTTP_ENDPOINT. Si viene de handlers por evento, hay que reescribirla como servidor. En los dos casos no conviene cambiar todo de golpe: las revisiones y los pesos por revisión del endpoint permiten hacer un canary (mandar un porcentaje del tráfico a la versión nueva y volver atrás si algo falla), que es lo que recomienda la guía de buenas prácticas para producción. Y antes de migrar, conviene preguntarse si hay algo para ganar: un endpoint de bajo volumen con request/response simple no mejora.
Un punto importante, aunque lo nombre muchas veces en estas notas, es que hoy solo ofrece runtime con node.js.
Medir antes de decidir
Los números del playground salen de una simulación. Sirven para entender el modelo, no para cotizar. La documentación recomienda hacer pruebas de carga con las mismas dependencias que en producción, subiendo la concurrencia hasta ver qué se agota primero (latencia, CPU, memoria o la base de datos), y revisar antes las cuotas de la cuenta y de la región. Con esos resultados se ajustan maxConcurrencyPerEnvironment, maxEnvironments y rateLimit. El costo real hay que verlo en la factura: la memoria se cobra durante toda la vida del entorno, y lo que más mueve la CPU no es el trabajo por pedido (~9 ms en caliente en el BFF) sino cuántos entornos se abren, porque cada uno paga su arranque (~780 ms el primer pedido de un entorno frío).
Lecciones aprendidas
- La plataforma importa tu módulo a través de un wrapper, no lo ejecuta como entrypoint: el servidor tiene que arrancar al importarse.
-
/pingestá reservado por la plataforma; para el health check hay que usar otra ruta. - Toda dependencia que use el código, incluido el AWS SDK, tiene que estar declarada en el
package.json; si no, falla recién en runtime. - No hay afinidad de sesión y el entorno puede desaparecer en cualquier momento: el estado va afuera (S3, DynamoDB).
- Los log groups se crean sin expiración; conviene fijar la retención.
- Con SAM,
CodeUritiene que ser un objeto en un bucket S3 versionado:sam buildno empaqueta código local para este recurso. - Es un servicio nuevo: hacen falta versiones recientes del AWS CLI y de SAM CLI.


Top comments (0)