How to Build Real-Time Chat Apps with AWS WebSocket API Gateway Guide
By Braincuber Team
Published on May 9, 2026
Real-time communication is the backbone of modern chat applications, live dashboards, and collaborative tools. Most traditional apps rely on client-initiated requests, but real-time applications require the server to push data without the client asking first. AWS announced WebSocket support for API Gateway at re:Invent 2018 and launched it soon after, enabling developers to build two-way communication channels using fully serverless infrastructure. This complete tutorial walks you through building a functional chat application using AWS WebSocket API Gateway, Lambda, and DynamoDB from scratch.
What You Will Learn:
- How WebSocket APIs differ from traditional REST APIs and why they power real-time apps
- How to create a WebSocket API in AWS API Gateway with custom route selection
- How to manage connection lifecycles using $connect and $disconnect routes
- How to store and retrieve connection IDs in DynamoDB for broadcast messaging
- How to implement a custom onMessage route that broadcasts to all connected clients
- How to test your WebSocket API using the wscat CLI tool
- How to wire Lambda functions to post messages back to connected devices using the Connection URL
Prerequisites
| Requirement | Details |
|---|---|
| AWS Account | Active account with access to API Gateway, Lambda, DynamoDB, and IAM |
| Node.js and npm | Installed locally for running the wscat testing tool |
| wscat CLI Tool | Install via npm install -g wscat to test WebSocket connections |
| IAM Role | Lambda execution role with DynamoDB read/write and API Gateway management permissions |
| Basic JavaScript | Familiarity with Node.js Lambda functions and DynamoDB DocumentClient |
Understanding WebSocket API Concepts
A WebSocket API is composed of one or more routes. A route selection expression determines which route a particular inbound request should use by evaluating a property in the incoming message. For example, if your JSON messages contain an action property and you want to perform different actions based on this property, your route selection expression would be $request.body.action. When a JSON message like {"action": "onMessage", "message": "Hello everyone"} arrives, the onMessage route is automatically selected.
API Gateway provides three predefined routes that handle the connection lifecycle. In addition to these, you can define custom routes for your application-specific logic.
| Route | Purpose |
|---|---|
| $default | Catches requests when the route selection expression does not match any defined route key. Use for generic error handling or fallback logic. |
| $connect | Triggered when a client first establishes a WebSocket connection. Use to register the connection ID in your database. |
| $disconnect | Triggered when a client disconnects from the WebSocket. Use to clean up stale connection IDs from your database. |
| Custom routes | User-defined routes such as onMessage or sendMessage that handle application-specific WebSocket events. |
Once a device successfully connects through the WebSocket API, it is allocated a unique connection ID that persists for the lifetime of the connection. To send messages back to a specific device, you make a POST request to the Connection URL using this format:
POST https://{api-id}.execute-api.{region}.amazonaws.com/{stage}/@connections/{connection_id}
Connection ID Management
Every client gets a unique connection ID on connect. Store it in DynamoDB so you can send messages back to any connected device at any time during the session.
Route Selection Expression
Configure $request.body.action as your expression so that JSON messages with an action field are automatically routed to the matching custom route key.
DynamoDB for Connection State
A single DynamoDB table with connectionid as the primary key stores all active connections. The onMessage route scans the table and broadcasts to every connection.
Two-Endpoint Architecture
After deployment, you receive two URLs: the WebSocket URL for client connections and the Connection URL for the server to call back to connected devices.
Creating the WebSocket API in API Gateway
Navigate to the Amazon API Gateway service in the AWS Console. Choose to create a new API and select WebSocket. Give your API a descriptive name and set the Route Selection Expression to $request.body.action. This expression tells API Gateway to examine the action property of every incoming JSON message and route it to the corresponding route key. Click Create API to proceed.
Create the DynamoDB Table for Connection Storage
Navigate to DynamoDB in the AWS Console and create a new table named Chat. Set connectionid as the primary key with string type. This table acts as the connection registry for your real-time application. Every connected client gets an entry here, and disconnected clients are removed. The table uses minimal capacity since it stores only active connection IDs with no additional data. Use the default settings for read and write capacity.
Building the $connect Lambda Function
The connect Lambda function is triggered whenever a new client opens a WebSocket connection. Its sole responsibility is to extract the connectionId from the incoming event and persist it in the DynamoDB Chat table. The function needs IAM permissions to perform PutItem operations on DynamoDB.
Create and Deploy the ChatRoomConnectFunction Lambda
Create a new Lambda function named ChatRoomConnectFunction with a Node.js runtime. Assign an IAM role that has permissions to put items in DynamoDB and invoke API Gateway endpoints. The handler reads event.requestContext.connectionId and calls DynamoDB put to store the connection. Return a 200 status code for successful registration.
const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient();
exports.handler = (event, context, callback) => {
const connectionId = event.requestContext.connectionId;
addConnectionId(connectionId).then(() => {
callback(null, {
statusCode: 200,
});
});
}
function addConnectionId(connectionId) {
return ddb.put({
TableName: 'Chat',
Item: {
connectionid: connectionId
},
}).promise();
}
IAM Permissions Required
Your Lambda execution role must include three types of permissions: DynamoDB access (GetItem, PutItem, DeleteItem, Scan on the Chat table), API Gateway Management API access (execute-api:ManageConnections and execute-api:Invoke), and the standard CloudWatch Logs permissions for function logging. Without execute-api:ManageConnections, your Lambda cannot send messages back to connected clients.
Building the $disconnect Lambda Function
The disconnect Lambda handles cleanup. When a client closes the connection, this function removes the client's connection ID from the DynamoDB Chat table, preventing stale entries from accumulating. Stale entries would cause broadcast attempts to fail for disconnected clients.
Create the ChatRoomDisconnectFunction Lambda
The disconnect function uses DynamoDB delete to remove the connection ID entry. Create it with a Node.js runtime and grant it the same DynamoDB and API Gateway permissions as the connect function. The clean-up logic only needs the connectionId from the event context to perform the deletion. Return a 200 status code to confirm the disconnection was processed.
const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient();
exports.handler = (event, context, callback) => {
const connectionId = event.requestContext.connectionId;
deleteConnectionId(connectionId).then(() => {
callback(null, {
statusCode: 200,
});
});
}
function deleteConnectionId(connectionId) {
return ddb.delete({
TableName: 'Chat',
Key: {
connectionid: connectionId,
},
}).promise();
}
Wiring Lambda Functions to WebSocket Routes
After creating both Lambda functions and the DynamoDB table, you need to configure the routes inside API Gateway. Navigate to the Routes section of your WebSocket API. Click on the $connect route, select Lambda Function as the integration type, and choose ChatRoomConnectFunction. Repeat the same process for the $disconnect route, selecting ChatRoomDisconnectFunction as the integration target. No additional proxy or request templates are required since the raw event data is passed directly to your Lambda handlers.
Testing $connect and $disconnect with wscat
With both routes configured, deploy the API from the Actions menu. Choose Deploy API and create a new stage like Test. After deployment succeeds, API Gateway displays two URLs: the WebSocket URL (for client connections) and the Connection URL (for server callbacks). Install the wscat CLI tool globally using npm and connect to your API using the WebSocket URL.
npm install -g wscat
wscat -c wss://{api-id}.execute-api.us-east-1.amazonaws.com/Test
When the connection succeeds, you will see a Connected message in the terminal. To verify the connect Lambda worked correctly, navigate to the DynamoDB console, open the Chat table, and inspect the items. You should see a new entry with the connection ID of the connected terminal. To test the disconnect logic, press CTRL + C in the terminal, which simulates a client disconnection. The disconnect Lambda removes the entry from the DynamoDB table automatically.
Implementing the Custom onMessage Route for Broadcast Messaging
The onMessage route is the core of the chat application. When a connected client sends a JSON message with {"action": "onMessage", "message": "..."}, this route's Lambda function retrieves all active connection IDs from DynamoDB and broadcasts the message to every connected client using the Connection URL POST endpoint.
Create ChatRoomOnMessageFunction with Broadcast Logic
This Lambda uses the ApiGatewayManagementApi client to post messages back to connected devices. It initializes the client using event.requestContext.domainName and event.requestContext.stage. The function parses the incoming JSON body to extract the message field, then scans the DynamoDB Chat table and iterates over every connectionId, calling postToConnection for each one. Since WebSocket support was new at launch, a patch file manually registers the service definition in the AWS SDK.
const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient();
require('./patch.js');
let send = undefined;
function init(event) {
console.log(event);
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: event.requestContext.domainName + '/' + event.requestContext.stage
});
send = async (connectionId, data) => {
await apigwManagementApi.postToConnection({
ConnectionId: connectionId,
Data: 'Echo: ' + data
}).promise();
};
}
exports.handler = (event, context, callback) => {
init(event);
let message = JSON.parse(event.body).message;
getConnections().then((data) => {
console.log(data.Items);
data.Items.forEach(function(connection) {
console.log("Connection " + connection.connectionid);
send(connection.connectionid, message);
});
});
return {};
};
function getConnections() {
return ddb.scan({
TableName: 'Chat',
}).promise();
}
SDK Patch Required for ApiGatewayManagementApi
At the time of writing, the WebSocket API was brand new and the ApiGatewayManagementApi service was not yet included in the standard AWS SDK. You need to create a patch.js file that manually registers the service definition, including the PostToConnection operation, the request URI format, and the required ConnectionId and Data parameters. The patch must be loaded before initializing the management API client via require('./patch.js'). In modern SDK versions, this patch is no longer required.
The Patch.js File for SDK Registration
Create a file named patch.js in the same directory as your Lambda deployment package. This file programmatically registers the ApiGatewayManagementApi service in the AWS SDK. It defines the service endpoint prefix, the PostToConnection operation, and the input structure including the required ConnectionId and Data parameters. The patch also configures the signing name as execute-api and specifies the signature version as v4. This file is loaded before any code that initializes the ApiGatewayManagementApi client.
require('aws-sdk/lib/node_loader');
var AWS = require('aws-sdk/lib/core');
var Service = AWS.Service;
var apiLoader = AWS.apiLoader;
apiLoader.services['apigatewaymanagementapi'] = {};
AWS.ApiGatewayManagementApi = Service.defineService(
'apigatewaymanagementapi', ['2018-11-29']
);
Object.defineProperty(
apiLoader.services['apigatewaymanagementapi'],
'2018-11-29', {
get: function get() {
var model = {
"metadata": {
"apiVersion": "2018-11-29",
"endpointPrefix": "execute-api",
"signingName": "execute-api",
"serviceFullName": "AmazonApiGatewayManagementApi",
"serviceId": "ApiGatewayManagementApi",
"protocol": "rest-json",
"jsonVersion": "1.1",
"uid": "apigatewaymanagementapi-2018-11-29",
"signatureVersion": "v4"
},
"operations": {
"PostToConnection": {
"http": {
"requestUri": "/@connections/{connectionId}",
"responseCode": 200
},
"input": {
"type": "structure",
"members": {
"Data": { "type": "blob" },
"ConnectionId": {
"location": "uri",
"locationName": "connectionId"
}
},
"required": ["ConnectionId", "Data"],
"payload": "Data"
}
}
},
"shapes": {}
};
model.paginators = { "pagination": {} };
return model;
},
enumerable: true,
configurable: true
});
module.exports = AWS.ApiGatewayManagementApi;
Configuring the Custom onMessage Route
Return to API Gateway and navigate to the Routes page. Create a new custom route by entering onMessage as the Route Key. Select Lambda Function as the integration type and choose the ChatRoomOnMessageFunction. With all three routes now configured, redeploy the API to make the onMessage route active.
Deploy the API and Test Full Broadcast Messaging
After redeploying, open two or more terminal windows and connect each one to the WebSocket API using wscat. Once all terminals show Connected, send a JSON message from one terminal: {"action": "onMessage", "message": "Hello everyone"}. The action field triggers the onMessage route, and the message is broadcast to all connected terminals. Each terminal displays the echoed message broadcast by the Lambda function. This confirms that the real-time broadcast pipeline is fully functional.
{"action": "onMessage", "message": "Hello everyone"}
The key relationship is: the action field in the JSON maps to the custom route name, and the message field is the data payload delivered to all connected clients. This routing mechanism makes it easy to extend the application with additional routes such as privateMessage, typingIndicator, or joinRoom without changing the existing infrastructure.
Architecture Summary: How Components Interact
| Component | AWS Service | Responsibility |
|---|---|---|
| WebSocket Endpoint | API Gateway (WebSocket) | Manages persistent client connections and routes messages by action key |
| Connection Registry | DynamoDB (Chat Table) | Stores active connection IDs for broadcast; prunes disconnected IDs |
| Connect Handler | Lambda (ConnectFunction) | Registers new connection ID in DynamoDB on $connect route |
| Disconnect Handler | Lambda (DisconnectFunction) | Removes stale connection ID from DynamoDB on $disconnect route |
| Message Broadcaster | Lambda (OnMessageFunction) | Scans all connections and pushes message via Connection URL callback |
| Test Client | wscat CLI (npm package) | Opens WebSocket connections and sends JSON messages for testing |
$default Route Not Implemented
The $default route is called when no matching route key is found for an incoming message. This tutorial does not implement the $default route, leaving it as an exercise for you. Use it to implement generic error handling, return meaningful error messages to clients sending invalid action keys, or log unexpected message formats for debugging purposes.
Frequently Asked Questions
How many concurrent WebSocket connections can API Gateway support?
API Gateway WebSockets scale automatically with no connection limits imposed by the service itself. The practical limit depends on the backend: DynamoDB throughput for storing connection IDs and Lambda concurrency for handling onMessage broadcasts. Configure DynamoDB auto-scaling and increase Lambda reserved concurrency for high-traffic scenarios.
How does WebSocket API pricing compare to REST API Gateway?
WebSocket APIs charge for the number of messages sent and received plus connection duration minutes. The pricing model is different from REST APIs since connections remain open. For chat applications with infrequent messages but long-lived connections, connection duration costs dominate. Evaluate your message frequency and connection count patterns before deploying to production.
Is the patch.js file still needed with modern AWS SDK versions?
No, the patch.js file was required only because the WebSocket API launched in December 2018 while the AWS SDK for Node.js had not yet been updated to include the ApiGatewayManagementApi service. Modern SDK versions include this service natively. If using recent AWS SDK, you can skip the patch entirely and use the management API client directly.
Can I use DynamoDB Scan for production broadcast at scale?
Scan works for small to medium connection counts but becomes expensive at scale because it reads every item regardless of relevance. For large deployments, consider adding a GSI with a partition key like roomId, or use ElastiCache Redis for connection state management with pub/sub channels. DynamoDB Scan is appropriate for the demo scale shown in this tutorial.
How do I add authentication to WebSocket connections in API Gateway?
API Gateway WebSocket supports Lambda authorizers and IAM authorization on the $connect route. Implement a Lambda authorizer that validates a query parameter token or header before allowing the connection. The authorizer can reject unauthorized connections by returning a non-200 response, preventing the connect Lambda from executing for unauthenticated clients.
Need Help Building Real-Time Serverless Applications?
Our cloud experts can help you architect, deploy, and scale WebSocket-based applications on AWS using API Gateway, Lambda, and DynamoDB with best practices for production workloads.
