06.B
Realtime · WS + Lambda + DDB
A serverless WebSocket fan-out: API Gateway WS, $connect/$disconnect/sendMessage Lambdas, a Connections table in DynamoDB.
$ws.demo --connect 2 --send 1
>7 stages · pay-per-message · zero idle EC2
step 1 / 7
Client A
openwss://api.example.com
inbox
(empty)
Client B
—wss://api.example.com
inbox
(empty)
API Gateway · WS
$connect $disconnect sendMessage
$connect
lambda
sendMessage
lambda · fan-out
$disconnect
lambda
Connections
ddb · table · pk:connectionId
ABC123 · A · ts=…
// what's on the wire right now
▸ A → APIGW.WS · CONNECT
// the fan-out lambda
● lambda · sendMessage.js
// sendMessage Lambda (Node.js)
exports.handler = async (event) => {
const { connectionId } = event.requestContext;
const body = JSON.parse(event.body);
const conns = await ddb.scan({
TableName: "Connections",
}).promise();
const api = new ApiGatewayManagementApi({
endpoint: `${event.requestContext.domainName}/${event.requestContext.stage}`,
});
await Promise.allSettled(conns.Items.map(c =>
api.postToConnection({
ConnectionId: c.connectionId,
Data: JSON.stringify({ from: connectionId, body: body.text }),
}).promise().catch(err => {
// GoneException -> connection dropped, clean it up
if (err.statusCode === 410) {
return ddb.delete({
TableName: "Connections",
Key: { connectionId: c.connectionId },
}).promise();
}
})
));
return { statusCode: 200 };
};API Gateway gives every WebSocket connection a stable connectionId. We persist it in DDB on $connect, delete it on $disconnect, and use it in sendMessage to push messages back via the Management API.
Fan-out is a parallel postToConnection over all live connectionIds. 410 GoneException means the client dropped without telling us — we self-clean by deleting the row.
Why this is nice: pay per message, no idle EC2, and DDB single-table design scales horizontally. Same shape powers most of the real-time features I've worked on.