Dai Codes

A software engineerʼs blog

Menu
Back
Photo of a child screaming into a microphone in a recording studio

Websocket broadcast issues with AWS gateway and nodejs lambdas

TL;DR: If your websocket clients seem to be missing some messages, make sure you're correctly awaiting calls to the AWS gateway management API in your lambda

I had a bit of a head-scratcher recently when working for the first time with AWS gateway websockets and nodejs (TypeScript) lambdas.

I was building a multiplayer game with the requirement to broadcast updates to all connected players. Everything was working perfectly when running the app locally using serverless offline, but when the app was deployed to AWS, some clients were periodically not receiving the messages.

First, let's take a little look at my broadcast function:

import { ApiGatewayManagementApi } from 'aws-sdk';
import { PushMessage } from '../model/messages';

export async function broadcast(
gateway: ApiGatewayManagementApi,
connectionIds: string[],
payload: PushMessage,
) {
const Data = JSON.stringify(payload);
await Promise.allSettled(
connectionIds.map((ConnectionId) =>
gateway
.postToConnection({
ConnectionId,
Data,
})
.promise()
.catch((e) => {
console.error('Broadcast error', e);
}),
),
);
}

This function takes a payload and an array of websocket connection IDs, and maps that to a bunch of Promises which represent the async calls to the AWS gateway management API's postToConnection endpoint.

The function itself is async and awaits for all of these Promises to be settled before resolving.

I would then call this function from my various lambda handlers like this:

export const handler: APIGatewayProxyWebsocketHandlerV2 = async (event) => {

// ...the body of my lambda, then...

broadcast(gateway, connectionIds, updateMessage);

return {
statusCode: 200,
};
}

As mentioned, this worked perfectly when testing locally using serverless offline, but when deployed to AWS, some clients were not receiving some messages.

It appears that the serverless offline runtime will wait for all async tasks to complete before exiting, whereas the real AWS lambda runtime will exit as soon as the Promise returned by the async lambda handler function completes, killing all other async tasks that are in progress.

This caused a race condition where some requests to the AWS gateway management API from the broadcast function were effectively cancelled because the main lambda handler function had returned before they could complete, meaning some push messages would not get sent.

The fix was simple: Everywhere I called my broadcast function, I just had to be sure to await it's returned Promise:

await broadcast(gateway, connectionIds, updateMessage);

Support

If you've found my blog useful, please consider buying me a coffee to say thanks:

Discuss

Comments or questions? Find me on Twitter: @daiscog.