Construir APIs en Tiempo Real con WebSockets en Serverless
Cómo implementar comunicación bidireccional en tiempo real usando WebSockets con AWS API Gateway, Lambda, DynamoDB y mejores prácticas de gestión de conexiones.
Visión general
Los ciclos tradicionales de petición-respuesta HTTP son insuficientes para aplicaciones que requieren actualizaciones en vivo — salas de chat, dashboards en tiempo real, juegos multijugador, tickers de acciones y edición colaborativa. Los WebSockets proporcionan una conexión TCP persistente y bidireccional entre cliente y servidor, permitiendo que los mensajes fluyan en ambas direcciones sin la sobrecarga de handshakes repetidos.
En arquitecturas serverless, los WebSockets requieren gestión de estado de conexión porque las funciones Lambda son efímeras. AWS API Gateway WebSocket API maneja la capa de protocolo WebSocket, mientras que una tabla DynamoDB rastrea las conexiones activas. Las funciones Lambda procesan $connect, $disconnect y rutas personalizadas, transmitiendo mensajes a los IDs de conexión destino. Esta receta cubre la implementación completa desde la infraestructura hasta el código cliente.
Cuándo usarlo
Usa esta receta cuando:
- Construyas aplicaciones de chat, notificaciones en vivo o feeds en tiempo real
- Transmitas datos en vivo a dashboards o herramientas de monitoreo
- Implementes edición colaborativa o estado de juegos multijugador
- Reemplaces polling largo o SSE con una conexión persistente más eficiente
- Transmitas eventos desde servicios backend a clientes conectados
Solución
Infraestructura AWS (Terraform)
resource "aws_apigatewayv2_api" "websocket" {
name = "realtime-api"
protocol_type = "WEBSOCKET"
route_selection_expression = "$request.body.action"
}
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.websocket.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.websocket.invoke_arn
}
resource "aws_apigatewayv2_route" "connect" {
api_id = aws_apigatewayv2_api.websocket.id
route_key = "$connect"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
resource "aws_apigatewayv2_route" "disconnect" {
api_id = aws_apigatewayv2_api.websocket.id
route_key = "$disconnect"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
resource "aws_apigatewayv2_route" "sendmessage" {
api_id = aws_apigatewayv2_api.websocket.id
route_key = "sendMessage"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
Handler Lambda (Node.js)
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();
const apigw = new AWS.ApiGatewayManagementApi({
endpoint: process.env.WEBSOCKET_ENDPOINT
});
exports.handler = async (event) => {
const { routeKey, connectionId, domainName, stage } = event.requestContext;
switch (routeKey) {
case '$connect':
await dynamo.put({
TableName: process.env.CONNECTIONS_TABLE,
Item: {
connectionId,
domainName,
stage,
connectedAt: Date.now(),
}
}).promise();
return { statusCode: 200 };
case '$disconnect':
await dynamo.delete({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId }
}).promise();
return { statusCode: 200 };
case 'sendMessage':
const body = JSON.parse(event.body);
const connections = await dynamo.scan({
TableName: process.env.CONNECTIONS_TABLE
}).promise();
const sendPromises = connections.Items.map(async (conn) => {
try {
await apigw.postToConnection({
ConnectionId: conn.connectionId,
Data: JSON.stringify({
message: body.message,
sender: connectionId,
timestamp: Date.now()
})
}).promise();
} catch (e) {
if (e.statusCode === 410) {
await dynamo.delete({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId: conn.connectionId }
}).promise();
}
}
});
await Promise.all(sendPromises);
return { statusCode: 200 };
default:
return { statusCode: 400 };
}
};
Cliente (Navegador)
const ws = new WebSocket('wss://your-api-id.execute-api.us-east-1.amazonaws.com/production');
ws.onopen = () => {
ws.send(JSON.stringify({ action: 'sendMessage', message: 'Hello world!' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data.message);
};
ws.onerror = (error) => console.error('WebSocket error:', error);
ws.onclose = () => console.log('Connection closed');
Explicación
- WebSocket API Gateway: gestiona el handshake WebSocket, mantiene las conexiones abiertas y enruta los mensajes entrantes a Lambda basándose en la
route_selection_expression. Las rutas$connecty$disconnectson gestionadas por el sistema. - Persistencia de conexiones: DynamoDB almacena
connectionId,domainNameystagepara cada cliente conectado. Esto es necesario porque las funciones Lambda son stateless — no pueden mantener referencias de conexión en memoria. - Broadcasting: para enviar un mensaje a todos los clientes, escanea la tabla de conexiones y llama
postToConnectionpara cadaconnectionId. Maneja errores410 Goneeliminando conexiones obsoletas. - Consideraciones de escalado: el escaneo de DynamoDB para broadcasting es aceptable para audiencias pequeñas. Para miles de conexiones, usa DynamoDB streams, fan-out vía SNS/SQS, o particiona conexiones por sala/tema.
Variantes
| Plataforma | Servicio WebSocket | Almacenamiento de conexiones | Mejor para |
|---|---|---|---|
| AWS | API Gateway v2 | DynamoDB | Stack serverless completo |
| Azure | Azure Web PubSub | Redis / integrado | Ecosistemas .NET |
| GCP | Cloud Run + Socket.io | Firestore | Tiempo real basado en contenedores |
| Pusher | Pusher Channels | Gestionado | Prototipado rápido |
| Ably | Ably Platform | Gestionado | Escala empresarial |
Mejores prácticas
- Usa salas o canales: en lugar de transmitir a todas las conexiones, agrúpalas por tema, sala o usuario. Consulta solo las conexiones relevantes para reducir costos de DynamoDB y latencia.
- Maneja conexiones obsoletas: las conexiones pueden caer sin disparar
$disconnect. Escanea y limpia conexiones periódicamente más antiguas que un umbral de heartbeat. - Habilita logging de CloudWatch: registra
$connect,$disconnecte invocaciones de rutas personalizadas para debugging y monitoreo de salud de conexiones. - Asegura la conexión: valida tokens de autenticación en la ruta
$connectusando authorizers Lambda o lógica personalizada antes de permitir que el handshake WebSocket se complete. - Implementa lógica de reconexión: los clientes deberían reconectarse automáticamente con backoff exponencial si la conexión cae, resuscribiéndose a canales previos al reconectar.
Errores comunes
- Almacenar estado de conexión en memoria Lambda: las instancias Lambda son efímeras. Cualquier mapa de conexiones en memoria se pierde cuando el contenedor de la función se destruye. Siempre usa DynamoDB o Redis.
- Escanear DynamoDB para audiencias grandes: un escaneo completo de tabla en miles de conexiones es lento y costoso. Usa GSIs o streams para transmisiones dirigidas.
- Olvidar manejar errores
postToConnection410: cuando un cliente se desconecta abruptamente,postToConnectionarroja un error 410. Fallar en capturarlo y limpiar filtra registros de conexión. - No configurar
route_selection_expressionde API Gateway: sin$request.body.action, las rutas personalizadas comosendMessageno se evaluarán y los mensajes retornarán 400.
Preguntas frecuentes
P: ¿Cuántas conexiones concurrentes puede manejar API Gateway WebSockets? R: API Gateway tiene una cuota predeterminada de 10,000 conexiones concurrentes por región, escalable mediante solicitud a soporte AWS. Para mayor escala, considera Ably, Pusher o infraestructura autogestionada.
P: ¿Puedo usar WebSockets con HTTP API Gateway?
R: No. Los WebSockets requieren API Gateway v2 con protocol_type = "WEBSOCKET". Las APIs HTTP no soportan conexiones persistentes.
P: ¿Cómo envío un mensaje desde un servicio backend a un cliente específico?
R: Busca el connectionId del cliente en DynamoDB, luego llama postToConnection con ese ID. Almacena un mapeo entre ID de usuario y ID de conexión para búsquedas fáciles.
P: ¿Cuál es el timeout de inactividad para API Gateway WebSockets? R: 10 minutos de inactividad. Envía mensajes ping periódicos desde el cliente o servidor para mantener la conexión viva.
Recursos Relacionados
Build Serverless APIs with API Gateway
How to design, deploy, and manage serverless HTTP APIs using AWS API Gateway, Lambda, and function-as-a-service patterns.
RecipeBuild Event-Driven Serverless Architectures
How to design loosely coupled systems using serverless functions triggered by events from message queues, databases, and webhooks.
RecipeRun Scheduled Jobs with Serverless Functions
How to replace cron jobs with serverless scheduled functions for backups, reporting, cleanup, and periodic maintenance tasks.