Skip to main content

Before you upgrade AWS Lambda Node.js runtime

· 5 min read
Ivan Barlog
AWS Solutions Architect @ BeeSolve

Node.js 24 runtime is available on AWS Lambda from 25th November 2025. It removes support for callback-based function handlers. In this article I will go through things which you should know before you upgrade your runtime.

If you are using callback-based function handlers and you tried to upgrade runtime to Node.js 24 you've probably encountered an error telling you that callback-based function handlers are not supported anymore.

Whenever you use callback-based function handlers the context.callbackWaitsForEmptyEventLoop is set to true by default which essentially means that whenever you forget to await some async function Lambda would still wait for clearing the event loop meaning your code will be executed within single request/response of Lambda. Here is better explanation 1:

callbackWaitsForEmptyEventLoop – By default (true), when using a callback-based function handler, Lambda waits for the event loop to be empty after the callback runs before ending the function invoke. Set to false to send the response and end the invoke immediately after the callback runs instead of waiting for the event loop to be empty. Outstanding events continue to run during the next invocation. Note that Lambda supports callback-based function handlers for Node.js 22 and earlier runtimes only.

Without callbacks and this special setting you need to make sure you will await all your async functions.

Why is this important? Lambda usually don't wait for empty event loop which means that if you return from function handler the Lambda execution stops 2. In practice this means that Lambda freezes and everything what was in event loop is persisted.

Whenever you hit Lambda with another request within 5 minutes when Lambda is being warm the execution continues eg. the frozen state is restored and anything which was previously at the end of event loop is now at start.

This might cause very strange behaviour. Imagine we have following Lambda handler:

import type { APIGatewayProxyEventV2, Context } from "aws-lambda";

type ChatMessage = {
readonly userId: string;
readonly recipientId: string;
readonly message: string;
readonly sentAt: Date;
};

export const handler = async (
event: APIGatewayProxyEventV2,
context: Context,
) => {
const body = parseBody(event.body);
const message: ChatMessage = {
userId: body.userId,
recipientId: body.recipientId,
message: body.message,
sentAt: new Date(),
};

// explicitly missing `await` allowing asynchronous persistence
persistChatMessage(message);

return {
status: 200,
// optimistic response of message before it is persisted
body: JSON.stringify(message),
};
};

function parseBody(
body: APIGatewayProxyEventV2["body"],
): Omit<ChatMessage, "sentAt"> {
// implementation of parsing body with error handling
}

async function persistChatMessage(data: ChatMessage) {
// persist data to the database
}

The above code would work perfectly with callback-based handler but with Node.js 24 the persistChatMessage() won't be executed within the same request/response execution. This particular persistChatMessage() call will be executed on next request if that request comes within 5 minutes window. If no-one hits this particular warm lambda within this window the call won't be executed at all.

When you want to migrate to Node.js 24 you should make sure to go through your codebase and ensure that all async functions are awaited properly. Of course if you know what you are doing you might want to keep this behaviour.

In order to avoid bugs in future you might want to use ESLint require-await rule so no developer forgets their awaits.

What's your experience with migration to Node.js 24? How did you taclked this problem? Did you? Please leave the comment below.

Example of not awaited function behaviour.

Sample code with NodejsFunction in CDK:

new NodejsFunction(stack, "Test", {
code: Code.fromInline(`
let coldStart = true;

exports.handler = async(event) => {
console.log({coldStart,event});
if (coldStart === true) coldStart = false;
// not using await here on purpose
delayedLog();

return {coldStart};
}

async function delayedLog() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 100);
}).then(() => console.log("should be invoked not within cold start but in consecutive request"));
}
`),
handler: "index.handler",
runtime: Runtime.NODEJS_24_X,
architecture: Architecture.ARM_64,
});

After the deployment I've invoked the function twice. You can see in the logs that the not awaited function has been called only in the second invocation.

INIT_START Runtime Version: nodejs:24.v29	Runtime Version ARN: arn:aws:lambda:eu-central-1::runtime:58a37e8413ed69058c4ac3b1df642118591f17d40def93d6101f867c72cd03c2
START RequestId: 67bfb757-2e5b-4d57-b3c8-6d72ca8e8842 Version: $LATEST
2026-02-02T10:40:14.978Z 67bfb757-2e5b-4d57-b3c8-6d72ca8e8842 INFO { coldStart: true }
END RequestId: 67bfb757-2e5b-4d57-b3c8-6d72ca8e8842
REPORT RequestId: 67bfb757-2e5b-4d57-b3c8-6d72ca8e8842 Duration: 70.51 ms Billed Duration: 197 ms Memory Size: 128 MB Max Memory Used: 75 MB Init Duration: 126.22 ms
2026-02-02T10:40:17.722Z 67bfb757-2e5b-4d57-b3c8-6d72ca8e8842 INFO should be invoked not within cold start but in consecutive request
2026-02-02T10:40:17.722Z 99690ca5-b825-448e-bcc5-a06749fef0a3 INFO { coldStart: false }
START RequestId: 99690ca5-b825-448e-bcc5-a06749fef0a3 Version: $LATEST
END RequestId: 99690ca5-b825-448e-bcc5-a06749fef0a3
REPORT RequestId: 99690ca5-b825-448e-bcc5-a06749fef0a3 Duration: 16.85 ms Billed Duration: 17 ms Memory Size: 128 MB Max Memory Used: 75 MB
2026-02-02T10:40:25.312Z 99690ca5-b825-448e-bcc5-a06749fef0a3 INFO should be invoked not within cold start but in consecutive request

Footnotes

  1. for more information regarding see Using the Lambda context object to retrieve Node.js function information

  2. for more information regarding Lambda lifecycle see Understanding the Lambda execution environment lifecycle