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
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
};
More use cases
In Jeremy Daly’s blog, he shows how declaring variables outside the handler enables you to reuse database connections across multiple invocations.
AWS Powertools for TypeScript: The AWS Powertools Parameters utility uses the same approach.
Declaration of variable at the runtime level
...
const DEFAULT_PROVIDERS: Record<string, BaseProviderInterface> = {};
...
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);
};
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);
};
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
});
...
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');
};
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();
};
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.
Top comments (0)