DEV Community

Ali Haydar for AWS Community Builders

Posted on • Edited on

Optimise your AWS Lambda performance with NodeJS top-level await

A few months ago, I wrote an article about how to speed up your Lambda function, which explores cold starts a bit and talks about the init code phase and writing code outside the Lambda handler function.

I got a few comments asking how to call an async function outside the Lambda handler. There are a few use cases where this could be useful. For example, when retrieving a password from AWS Secrets Manager, loading a file from S3, or opening a connection to RDS database, it might not be required to do this operation upon every function invocation. So it would be great to run this operation during the function init.

const {
  SecretsManagerClient,
  GetSecretValueCommand,
} = require("@aws-sdk/client-secrets-manager");

const client = new SecretsManagerClient();
const input = {
  SecretId: "<SECRET_MANAGER_RESOURCE_ID>",
};

const command = new GetSecretValueCommand(input);
const response = await client.send(command); // This will throw an error

exports.handler = async (event) => {
    // TODO use the returned response
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
Enter fullscreen mode Exit fullscreen mode

This will throw the following error:

  "errorType": "Runtime.UserCodeSyntaxError",
  "errorMessage": "SyntaxError: await is only valid in async function",
Enter fullscreen mode Exit fullscreen mode

The only way we used to have to keep an optimal performance was to move const response = await client.send(command); into the Lambda handler. This implementation is Ok but not the best, because it will be executed upon every Lambda invocation, and it doesn't help with the readability of the code.

However, NodeJS introduced Top-Level await as an experimental feature in version 14, then it became generally available in version 16.

That's great. How can we use it? We need to change our code to use ES modules. You can do this by either changing the extension of your JS file to have the .mjs file extension or simply adding the "type": "module" to your package.json file.

The code would look as follows:

import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";

const client = new SecretsManagerClient();
const input = {
  SecretId: "<SECRET_MANAGER_RESOURCE_ID>",
};

const command = new GetSecretValueCommand(input);
const response = await client.send(command); // This will work well

export const handler = async (event) => {
    // TODO use the returned response
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

Enter fullscreen mode Exit fullscreen mode

Do you know of any other valuable tips with Lambda and NodeJS? I'd love to hear from you.


Thanks for reading this far. Did you like this article, and do you think others might find it useful? Feel free to share it on Twitter or LinkedIn.

Top comments (2)

Collapse
 
arcticshadow profile image
Cole Diffin

The 'old' way would be to have the function execute from inside the handler - but to delegate the retrieval to another function, that is an async function, that function can have a global variable reference to store the retreived value in. i.e.

const {
  SecretsManagerClient,
  GetSecretValueCommand,
} = require("@aws-sdk/client-secrets-manager");



// global value for keeping the response handy between lambda invocations
let clientResponse;

const getSecret = async () => {
  const client = new SecretsManagerClient();
  const input = {
    SecretId: "<SECRET_MANAGER_RESOURCE_ID>",
  };
  const command = new GetSecretValueCommand(input);
  clientResponse = await client.send(command); // This will throw an error
  return clientResponse
}

exports.handler = async (event) => {
    await getSecret();

    // TODO use the returned response
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
Enter fullscreen mode Exit fullscreen mode

There are a couple of benefits to still following this pattern - all your code is contained within functions - allowing better readability. You can structure you functions however you want (even in different files) - also keeps all of your concerns in one place. Now i know that all my secret retrieval code is in the getSecret function. Additionally, it limits the side affects to a single globally scoped variable, that is accessed form exactly one place. (This side affect can be additionally scoped down, if for example the getSecret function and its related global var was in its own file.

I cant find the reference right now, but past experiences prefer to have the aws clients initialised inside the handler function (i.e. so that get initialised every time) it makes testing them much easier.

I guess my takeaway is - just because you can do something, doesn't mean its always the right thing. There are many ways to solve problems, it's only when you understand the different ways, that you can choose the correct one.

Collapse
 
ahaydar profile image
Ali Haydar

Hey @arcticshadow, sorry I missed this comment from you, and just stumbled upon it. So responding now; better late than never.

I appreciate your feedback and you taking the time to explain a different implementation. I agree with what you're saying especially the piece that's related to testing, and keeping your code better organised within functions; usually, in commercial projects, the Lambda function does more than retrieving a secret, hence doing the kind of structuring you're suggesting is important.

In other instances, I encountered a need to invoke a tiny Lambda function quite often, and the cold start was starting to cause a performance issues). Moving the part of the code that was not "necessary to be executed every time" to the "init" code, before the handler, saved the "init" time for the following invokations of the Lambda. Assuming that code was taking 200ms, keeping it in the handler means it will always use these 200ms. However, moving it outside of it enabled saving this time in a few calls, while the Lambda was warm.

This might not be important in lots of places or not add a business value, and that's ok. I would iterate on what you said that there are different way to solve the problem. ❤️