AWS WebSocket API Gateway Guide
AWS WebSocket API Gateway enables you to build serverless, real-time applications without managing infrastructure. This service manages WebSocket connections at scale while integrating with AWS Lambda for business logic, making it a powerful solution for chat applications, live dashboards, multiplayer games, and collaborative tools.
This guide covers everything you need to build production-ready WebSocket APIs using AWS API Gateway, from connection management to message broadcasting. We’ll explore how api gateway websocket routes work, implement Lambda handlers, manage persistent connections, and deploy your infrastructure using modern tools.
How API Gateway WebSocket Works
AWS WebSocket API Gateway operates differently from traditional WebSocket servers. Instead of maintaining a persistent server process, API Gateway handles connection lifecycle events by invoking Lambda functions based on predefined routes.
When a client connects to your aws websocket api, three core route types determine how messages flow:
$connect Route: Invoked when a client establishes a WebSocket connection. This is where you authenticate users, validate connection parameters, and store connection metadata. If your Lambda function returns a non-200 status code, API Gateway rejects the connection.
$disconnect Route: Triggered when a client disconnects or the connection times out. Use this route to clean up resources and remove connection records from your database. Note that this route is best-effort, meaning it might not trigger in all scenarios like network failures.
$default Route: Handles all messages that don’t match custom routes. This acts as your catch-all message handler. You can also define custom routes based on message content, allowing you to route different message types to different Lambda functions.
The aws websocket api gateway maintains connections for up to 2 hours of idle time. After receiving a message, API Gateway invokes your Lambda function with the message payload and connection metadata. Your function processes the logic and can send responses back through the API Gateway Management API.
A critical difference from traditional WebSocket servers is that aws lambda websocket functions are stateless. Connection state must be persisted externally, typically in DynamoDB, to track active connections and enable message broadcasting.
Creating Your AWS WebSocket API
You can create an aws api gateway websocket through the AWS Console, AWS CLI, or infrastructure as code tools like SAM and CDK. We’ll focus on SAM (Serverless Application Model) for reproducible deployments.
First, create a SAM template that defines your WebSocket API and Lambda functions:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: WebSocket API with Lambda integration
Globals:
Function:
Timeout: 30
Runtime: nodejs18.x
Environment:
Variables:
CONNECTIONS_TABLE: !Ref ConnectionsTable
Resources:
WebSocketAPI:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: ChatWebSocketAPI
ProtocolType: WEBSOCKET
RouteSelectionExpression: "$request.body.action"
ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketAPI
RouteKey: $connect
AuthorizationType: NONE
OperationName: ConnectRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref ConnectIntegration
ConnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketAPI
Description: Connect Integration
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ConnectFunction.Arn}/invocations
DisconnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketAPI
RouteKey: $disconnect
AuthorizationType: NONE
OperationName: DisconnectRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref DisconnectIntegration
DisconnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketAPI
Description: Disconnect Integration
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DisconnectFunction.Arn}/invocations
DefaultRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketAPI
RouteKey: $default
AuthorizationType: NONE
OperationName: DefaultRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref DefaultIntegration
DefaultIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketAPI
Description: Default Integration
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DefaultFunction.Arn}/invocations
Deployment:
Type: AWS::ApiGatewayV2::Deployment
DependsOn:
- ConnectRoute
- DisconnectRoute
- DefaultRoute
Properties:
ApiId: !Ref WebSocketAPI
Stage:
Type: AWS::ApiGatewayV2::Stage
Properties:
StageName: production
Description: Production Stage
DeploymentId: !Ref Deployment
ApiId: !Ref WebSocketAPI
ConnectionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: WebSocketConnections
AttributeDefinitions:
- AttributeName: connectionId
AttributeType: S
KeySchema:
- AttributeName: connectionId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
ConnectFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/
Handler: connect.handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ConnectionsTable
DisconnectFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/
Handler: disconnect.handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ConnectionsTable
DefaultFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/
Handler: default.handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ConnectionsTable
- Statement:
- Effect: Allow
Action:
- execute-api:ManageConnections
Resource:
- !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketAPI}/*'
ConnectFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref ConnectFunction
Principal: apigateway.amazonaws.com
DisconnectFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref DisconnectFunction
Principal: apigateway.amazonaws.com
DefaultFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref DefaultFunction
Principal: apigateway.amazonaws.com
Outputs:
WebSocketURL:
Description: WebSocket URL
Value: !Sub wss://${WebSocketAPI}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
This template creates a complete websocket api gateway with DynamoDB for connection storage and proper IAM permissions. The RouteSelectionExpression determines how incoming messages map to routes based on the message body.
Deploy this template using the SAM CLI:
sam build
sam deploy --guided
The guided deployment prompts you for stack name, region, and confirmation settings. After deployment completes, you’ll receive your WebSocket URL in the format wss://{api-id}.execute-api.{region}.amazonaws.com/{stage}.
Implementing Lambda Handlers
Each route requires a Lambda handler that processes connection events and messages. Let’s implement handlers for all three core routes.
Connect Handler
The connect handler validates incoming connections and stores connection metadata:
// src/handlers/connect.js
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb');
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
exports.handler = async (event) => {
const connectionId = event.requestContext.connectionId;
const timestamp = Date.now();
// Extract query parameters for user identification
const queryParams = event.queryStringParameters || {};
const userId = queryParams.userId;
const username = queryParams.username || 'Anonymous';
// Store connection in DynamoDB
try {
await docClient.send(new PutCommand({
TableName: process.env.CONNECTIONS_TABLE,
Item: {
connectionId,
userId,
username,
connectedAt: timestamp,
ttl: Math.floor(timestamp / 1000) + 7200 // 2 hour TTL
}
}));
console.log(`Connection ${connectionId} stored for user ${username}`);
return {
statusCode: 200,
body: 'Connected'
};
} catch (error) {
console.error('Error storing connection:', error);
return {
statusCode: 500,
body: 'Failed to connect'
};
}
};
Returning a non-200 status code rejects the connection. You can implement WebSocket authentication here by validating JWT tokens passed as query parameters.
Disconnect Handler
The disconnect handler removes connections from DynamoDB:
// src/handlers/disconnect.js
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, DeleteCommand } = require('@aws-sdk/lib-dynamodb');
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
exports.handler = async (event) => {
const connectionId = event.requestContext.connectionId;
try {
await docClient.send(new DeleteCommand({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId }
}));
console.log(`Connection ${connectionId} removed`);
return {
statusCode: 200,
body: 'Disconnected'
};
} catch (error) {
console.error('Error removing connection:', error);
return {
statusCode: 500,
body: 'Failed to disconnect'
};
}
};
Default Message Handler
The default handler processes incoming messages and can send responses:
// src/handlers/default.js
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, GetCommand } = require('@aws-sdk/lib-dynamodb');
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require('@aws-sdk/client-apigatewaymanagementapi');
const dynamoClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(dynamoClient);
// Initialize API Gateway Management API client
const getApiGatewayClient = (event) => {
const endpoint = `https://${event.requestContext.domainName}/${event.requestContext.stage}`;
return new ApiGatewayManagementApiClient({ endpoint });
};
exports.handler = async (event) => {
const connectionId = event.requestContext.connectionId;
const body = JSON.parse(event.body || '{}');
// Get connection details
let connection;
try {
const result = await docClient.send(new GetCommand({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId }
}));
connection = result.Item;
} catch (error) {
console.error('Error fetching connection:', error);
}
// Process message based on action
const action = body.action || 'message';
const apiGateway = getApiGatewayClient(event);
try {
if (action === 'ping') {
// Respond with pong
await apiGateway.send(new PostToConnectionCommand({
ConnectionId: connectionId,
Data: JSON.stringify({ type: 'pong', timestamp: Date.now() })
}));
} else if (action === 'message') {
// Echo message back with sender info
const response = {
type: 'message',
username: connection?.username || 'Unknown',
message: body.message,
timestamp: Date.now()
};
await apiGateway.send(new PostToConnectionCommand({
ConnectionId: connectionId,
Data: JSON.stringify(response)
}));
}
return { statusCode: 200, body: 'Message processed' };
} catch (error) {
console.error('Error sending message:', error);
return { statusCode: 500, body: 'Failed to process message' };
}
};
The API Gateway Management API enables lambda websockets to send data back to clients. You must initialize the client with your API endpoint, which you construct from the event’s requestContext.
Connection Management with DynamoDB
Effective connection management is crucial for aws websocket lambda applications. DynamoDB provides fast, scalable storage for tracking active connections.
The connections table schema should include:
- connectionId (partition key): Unique identifier for each WebSocket connection
- userId: Application user identifier for targeting specific users
- username: Display name or metadata
- connectedAt: Timestamp for connection tracking
- ttl: Time-to-live for automatic cleanup
To retrieve all active connections for broadcasting:
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, ScanCommand } = require('@aws-sdk/lib-dynamodb');
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
async function getAllConnections() {
const result = await docClient.send(new ScanCommand({
TableName: process.env.CONNECTIONS_TABLE
}));
return result.Items || [];
}
For large-scale applications, consider adding a Global Secondary Index on userId to efficiently query all connections for a specific user. This enables targeted messaging without scanning the entire table.
DynamoDB’s TTL feature automatically removes stale connections. Set the ttl attribute to a Unix timestamp (in seconds) representing when the connection should expire. This handles cases where the $disconnect route doesn’t trigger.
Sending Messages Back to Clients
The API Gateway Management API is how aws api gateway websocket sends data to connected clients. This API is separate from the WebSocket API itself and requires specific IAM permissions.
Here’s a robust function for sending messages with error handling:
const { ApiGatewayManagementApiClient, PostToConnectionCommand, DeleteConnectionCommand } = require('@aws-sdk/client-apigatewaymanagementapi');
const { DynamoDBDocumentClient, DeleteCommand } = require('@aws-sdk/lib-dynamodb');
async function sendMessage(apiGateway, docClient, connectionId, data) {
try {
await apiGateway.send(new PostToConnectionCommand({
ConnectionId: connectionId,
Data: typeof data === 'string' ? data : JSON.stringify(data)
}));
return { success: true };
} catch (error) {
if (error.statusCode === 410) {
// Connection is stale (Gone)
console.log(`Removing stale connection ${connectionId}`);
// Clean up from DynamoDB
await docClient.send(new DeleteCommand({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId }
}));
return { success: false, stale: true };
}
console.error(`Error sending to ${connectionId}:`, error);
return { success: false, error };
}
}
A 410 (Gone) error indicates the connection no longer exists. Always catch this error and remove the connection from your database to prevent future send attempts.
The Data parameter accepts either a string or Buffer. For JSON messages, stringify objects before sending. Binary data can be sent as a Buffer for efficient transmission.
Broadcasting Messages to Multiple Connections
Broadcasting is a common requirement for chat applications and live updates. With aws websocket api, broadcasting requires iterating through connections and sending messages individually.
Here’s a complete broadcast handler:
// src/handlers/broadcast.js
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, ScanCommand, DeleteCommand } = require('@aws-sdk/lib-dynamodb');
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require('@aws-sdk/client-apigatewaymanagementapi');
const dynamoClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(dynamoClient);
const getApiGatewayClient = (event) => {
const endpoint = `https://${event.requestContext.domainName}/${event.requestContext.stage}`;
return new ApiGatewayManagementApiClient({ endpoint });
};
exports.handler = async (event) => {
const connectionId = event.requestContext.connectionId;
const body = JSON.parse(event.body || '{}');
// Get sender info
const senderResult = await docClient.send(new GetCommand({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId }
}));
const sender = senderResult.Item;
// Get all connections
const connectionsResult = await docClient.send(new ScanCommand({
TableName: process.env.CONNECTIONS_TABLE
}));
const connections = connectionsResult.Items || [];
const apiGateway = getApiGatewayClient(event);
const message = {
type: 'broadcast',
username: sender?.username || 'Anonymous',
message: body.message,
timestamp: Date.now()
};
const messageData = JSON.stringify(message);
// Send to all connections
const sendPromises = connections.map(async (connection) => {
try {
await apiGateway.send(new PostToConnectionCommand({
ConnectionId: connection.connectionId,
Data: messageData
}));
} catch (error) {
if (error.statusCode === 410) {
// Remove stale connection
await docClient.send(new DeleteCommand({
TableName: process.env.CONNECTIONS_TABLE,
Key: { connectionId: connection.connectionId }
}));
}
}
});
await Promise.all(sendPromises);
return { statusCode: 200, body: 'Broadcast sent' };
};
For high-volume broadcasting, consider batching operations and implementing pagination if your connections exceed DynamoDB’s 1MB scan limit. You can also use DynamoDB Streams to trigger broadcast Lambda functions when specific data changes occur.
Authentication and Authorization
Securing your websocket api gateway is critical for production applications. AWS provides several authentication mechanisms.
Lambda Authorizers
Lambda authorizers validate tokens before allowing connections:
// src/handlers/authorizer.js
const jwt = require('jsonwebtoken');
exports.handler = async (event) => {
const token = event.queryStringParameters?.token;
if (!token) {
return generatePolicy('user', 'Deny', event.methodArn);
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Return policy allowing connection
return generatePolicy(decoded.userId, 'Allow', event.methodArn, {
userId: decoded.userId,
username: decoded.username
});
} catch (error) {
console.error('Token verification failed:', error);
return generatePolicy('user', 'Deny', event.methodArn);
}
};
function generatePolicy(principalId, effect, resource, context = {}) {
return {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource
}]
},
context
};
}
Add the authorizer to your SAM template:
WebSocketAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
Name: LambdaAuthorizer
ApiId: !Ref WebSocketAPI
AuthorizerType: REQUEST
AuthorizerUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizerFunction.Arn}/invocations
IdentitySource:
- route.request.querystring.token
ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketAPI
RouteKey: $connect
AuthorizationType: CUSTOM
AuthorizerId: !Ref WebSocketAuthorizer
The authorizer runs before the $connect handler, blocking unauthorized connections. Context values from the authorizer become available in subsequent Lambda invocations through event.requestContext.authorizer.
For more authentication strategies, see our guide on WebSocket authentication.
IAM-Based Authentication
For service-to-service communication, use IAM authentication with SigV4 signing. This requires clients to sign WebSocket requests using AWS credentials:
ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketAPI
RouteKey: $connect
AuthorizationType: AWS_IAM
Clients must use libraries that support SigV4 signing for WebSocket connections, such as the AWS IoT Device SDK or custom implementations.
Costs and Limits
Understanding aws websocket pricing and limits helps you design cost-effective applications.
Pricing
AWS WebSocket API Gateway charges for:
- Messages: $1.00 per million messages (first 1 billion messages per month)
- Connection minutes: $0.25 per million connection minutes
A connection minute is counted in one-minute increments. A client connected for 30 seconds consumes one connection minute.
Lambda costs are separate:
- Requests: $0.20 per million requests
- Duration: $0.0000166667 per GB-second
DynamoDB costs depend on your billing mode:
- On-demand: $1.25 per million write requests, $0.25 per million read requests
- Provisioned: Based on configured capacity units
Limits
Key limits for aws lambda websocket applications:
- Connection duration: Maximum 2 hours idle timeout
- Message size: 128 KB per message (after base64 encoding for binary data)
- Connection limit: 10,000 connections per account per region (can be increased)
- Lambda timeout: Maximum 29 seconds for route handlers
- Rate limits: 10,000 requests per second per account per region
For applications requiring longer message processing, use asynchronous patterns. Have your Lambda function return immediately while processing continues in the background, then send results back through the Management API.
When you approach limits, request increases through AWS Support or architect around them using multiple APIs or regions.
AWS AppSync as an Alternative
While this guide focuses on api gateway websocket, AWS AppSync provides another option for real-time applications. AppSync websocket support is built on GraphQL subscriptions rather than raw WebSocket messages.
AppSync advantages:
- GraphQL-based data fetching with subscriptions
- Automatic connection management
- Built-in DynamoDB integration with resolvers
- Fine-grained authorization with multiple auth modes
AppSync disadvantages:
- Less flexibility for custom protocols
- GraphQL overhead for simple use cases
- Higher costs for high-volume messaging
Choose appsync websocket when you want GraphQL benefits and automatic data synchronization. Use API Gateway WebSocket when you need full protocol control, custom message formats, or integration with existing WebSocket clients.
Both services can coexist in your architecture. Use API Gateway for custom real-time features and AppSync for data-driven subscriptions.
Testing Your WebSocket API
Proper testing ensures reliability before production deployment. For comprehensive strategies, see our guide on how to test WebSockets.
Quick testing with wscat:
npm install -g wscat
# Send a message
> {"action": "message", "message": "Hello from wscat"}
# You'll receive the response
< {"type":"message","username":"TestUser","message":"Hello from wscat","timestamp":1708000000000}
For automated testing, use the ws library:
const WebSocket = require('ws');
const ws = new WebSocket('wss://your-api-id.execute-api.us-east-1.amazonaws.com/production?userId=123');
ws.on('open', () => {
console.log('Connected');
ws.send(JSON.stringify({ action: 'message', message: 'Test message' }));
});
ws.on('message', (data) => {
console.log('Received:', data.toString());
});
ws.on('close', () => {
console.log('Disconnected');
});
Monitor your API using CloudWatch Logs and metrics. Enable execution logging in your API Gateway stage settings to debug message routing and Lambda invocations.
Frequently Asked Questions
How do I handle WebSocket reconnections in AWS API Gateway?
AWS API Gateway does not automatically reconnect clients. Implement reconnection logic in your client application with exponential backoff. When a client reconnects, it receives a new connectionId, so your application must handle re-establishing user sessions. Store session state in DynamoDB using userId as the key rather than connectionId, allowing you to associate new connections with existing sessions.
Can I use AWS WebSocket API Gateway with languages other than Node.js?
Yes, you can implement lambda websockets in any language supported by AWS Lambda, including Python, Java, Go, C#, and Ruby. The API Gateway Management API has SDKs for all major languages. The connection event structure and response format remain the same regardless of runtime. Choose the language that best fits your team’s expertise and performance requirements.
What is the maximum number of concurrent connections for an AWS WebSocket API?
The default limit is 10,000 concurrent connections per account per region. You can request increases through AWS Support for production workloads. For applications exceeding this limit, consider partitioning users across multiple WebSocket APIs or regions. Large-scale applications often use a combination of API Gateway for control plane operations and custom infrastructure for high-volume data plane messaging.
How do I debug failed message deliveries in my WebSocket API?
Enable CloudWatch Logs execution logging in your API Gateway stage settings. This logs all route invocations, Lambda responses, and Management API calls. Use structured logging in your Lambda functions to track message flow. Implement dead-letter queues (DLQ) for Lambda functions to capture failed invocations. Monitor the IntegrationError and ExecutionError CloudWatch metrics to identify patterns in failures. For 410 (Gone) errors, ensure your disconnect handler and stale connection cleanup logic work correctly.