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
open
wss://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.

ws --connect api-gateway