DEV Community

How to reuse variables in your Lambda across invocations and know when it's about to terminate

Reader: Why do I need to know this? Can't we just jump straight into the code?

Writer: Understanding AWS Lambda lifecycles is like learning how to navigate a river. The river flows continuously, with twists, turns, and varying currents. If you don't understand the flow, you might find yourself fighting against it or getting stuck. But if you know how it moves, when it speeds up, slows down, or where it pools.  You can align with it and let it carry you where you need to go.

Reader: Alright, let's go with your analogy.

Writer: Here it is! the Lambda execution environment lifecycle

Image description

Reader: Woooow, I always wanted to read the official docs paraphrased by a junior dev. 

Writer: Ignoring the jab Let me explain the lifecycle in simple terms.

The Init Phase and Variable Reuse

The init phase occurs only during a cold start. When a new Lambda execution environment is created. During this phase the lambda performs 3 tasks:

  • Starts up extensions
  • Sets up the runtime
  • Runs any initialization code placed outside the handler function

This initialization is important because the variables and resources you set up here can be reused across multiple function invocations.

Example Caching
In the code below, we store a token at the module level. The first time the Lambda environment is created, we fetch the token and save it. On subsequent invocations (warm starts), we simply reuse the token, saving time and resources.

let token: Token;

const getToken = async (): Token => {
  if (token) {
    console.log('Reusing Token');
    return token;
  }
  console.log('Generating Token');
  token = await getTokenFromThirdPartyAPI();
  return token;
};

export const handler = async () => {
  const myToken = await getToken();
  // Use myToken as needed
};
Enter fullscreen mode Exit fullscreen mode

More use cases

Declaration of variable at the runtime level

...
const DEFAULT_PROVIDERS: Record<string, BaseProviderInterface> = {};
...
Enter fullscreen mode Exit fullscreen mode

Application of the singleton Pattern to reuse the variable if it has not already initialized

const getSecret = async (...) => {
  if (!Object.hasOwn(DEFAULT_PROVIDERS, 'secrets')) {
    DEFAULT_PROVIDERS.secrets = new SecretsProvider();
  }

  return (DEFAULT_PROVIDERS.secrets as SecretsProvider).get(name, options);
};
Enter fullscreen mode Exit fullscreen mode

Usage inside the handler

import { getSecret } from '@aws-lambda-powertools/parameters/secrets';

export const handler = async (): Promise<void> => {
  // Retrieve a secret, cached for 1 hour.
  const secret = await getSecret('my-secret', {
    maxAge: 3600,
  });
  console.log(secret);
};
Enter fullscreen mode Exit fullscreen mode

The Invoke Phase

The invoke phase is what happens every time your Lambda function is called(if it did not failed in the Init phase). This runs inside the handler function

The shutdown phase and how to know when your lambda is terminating

When the Lambda environment is about to be terminated, there’s a shutdown phase. If you have registered external or internal extensions, AWS will send a SIGTERM signal, giving you about 300ms (or 500ms for internal extensions) to gracefully shut down.

You can use any extension, in my case for the following example I've decided to use the Cloudwatch Lambda Insights Extensions, this extension allows you to get more data of your metrics and logs of a function.

In your CDK app


/// In your CDK App 
....
    const dummyFunction = new NodejsFunction(this, 'dummyFunction', {
      entry: path.join(__dirname, './src/sendAppleSessionRequest.ts'),
      insightsVersion: LambdaInsightsVersion.VERSION_1_0_333_0,  // Setup of Lambda Insights extension
    });

...
Enter fullscreen mode Exit fullscreen mode

In your Lambda


process.on('SIGTERM', async () => {
  console.log('Terminating Lambda');
  // Close connections
  // Close any open resources
  // Close DB connections
  // Terminate any ongoing processes
});

export const handler = async (
  event: any
) => {
  console.log('Starting lambda');
};
Enter fullscreen mode Exit fullscreen mode

And if you combine it with the power of reuse variables from the runtime you can get something pretty cool.

process.on('SIGTERM', async () => {
  console.log('Terminating Lambda');
  console.log('Closing connection');
});

let isConnectionInitialized: boolean;

const initializeConnection = async () => {
  if (!isConnectionInitialized) {
    console.log('Initializing connection');
    isConnectionInitialized = true;
  } else {
    console.log('Connection already initialized');
  }
};

export const handler = async (event: any) => {
  console.log('Starting Lambda');
  await initializeConnection();
};
Enter fullscreen mode Exit fullscreen mode

Summary

AWS Lambda functions run through three main phases: init, invoke, and shutdown.
By setting up and reusing variables during init, you save time and resources during each invocation.
With a graceful shutdown, you ensure everything wraps up cleanly before the next run.

Recommended reading

Top comments (0)