Why Should You Care About Lambda Lifecycle As A .NET Developer?
The Lambda Lifecycle affects the way we write out Function code. Learn some of the dos and don'ts when building Lambda Functions in .NET because of how Lambda initializes the Function classes.
Table of Contents
This article is sponsored by AWS and is part of my AWS Series.
I made a terrible mistake when I first started using AWS Lambda Functions.
I used an instance variable in my .NET class to store the Function state and reset it in the class Constructor code. I was expecting the state to reset for each function invocation.
But, the Function state was not getting reset on each invocation.
Understanding the AWS Lambda Lifecycle is essential when building Lambda Functions.
It would be best to avoid certain things when writing your Function code; we will go over those and why in this article.
AWS Lambda Function Lifecycle
AWS Lambda provides a secure and isolated runtime environment for our function code. This execution environment manages the resources required to run our Function and also provides lifecycle support for the Function’s runtime.
The Lambda Lifecycle mainly consists of three phases:
- Init Phase → Limited to 10 seconds. Starts all extension, runs Function’s static code, etc
- Invoke Phase → Invokes the Function handler configured when setting up Lambda Function and waits for the duration of the Functions timeout.
- Shutdown Phase → Limited to 2 seconds.
You can read more about each phase and the related activities in the official documentation here.
Lambda Lifecycle and .NET Functions
The Lambda Lifecycle affects the way we write our Function code.
Especially when using .NET, where you define the Function code within a Class. In .NET, you can define instance variables - properties at your Class level scope.
Let’s consider a simple AWS Lambda Function code, as shown below.
public class Function
{
public int Count { get; set; }
public Function()
{
Count = 0;
}
public string FunctionHandler(string input, ILambdaContext context)
{
Count++;
return $"{input} - {Count}";
}
}
We have a class Function
where we have the Lambda Function Handler (FunctionHandler
) which takes in string input. The Function
class also defines a Count
property that is initialized inside the Class constructor.
Since the FunctionHandler
increments the Count
property every time it’s called, the return value depends on whether you are invoking the Function on the same instance or different instances
I was expecting the Count
property to reset every time the Lambda Function was invoked because of the initialization code in the constructor. (Now, in my original scenario where I encountered this problem, I was doing much more than just incrementing a counter.)
But, like I said, it wasn’t. Why?
.NET Constructor and Lambda Init Phase
For .NET Functions, a new instance of the Function class gets created in the Init Phase. This instance is then stored and used in the Invoke phases, where all it does is pass on the request to the Function handler.
For any new Invoke requests, Lambda uses any free instances and invokes the function method on it.
AWS Lambda creates new instances of .NET class in the Init phase and reuses the instance in the Invoke Phase.
This means any initialization code in the class constructor is only called once when the instance is created. Any state maintained at the class/instance level must be reset appropriately inside the Function handler method.
So in our example before, if we invoke the Lambda Function multiple times, it keeps incrementing the Count
property and returns the value accordingly.
What Triggers New .NET Instances To Be Created?
All existing instances are removed whenever you change your function code and deploy the changes to AWS Lambda.
Any new requests coming to the AWS Lambda will create a new instance with the newly deployed code.
New instances are also automatically created when Lambda receives more requests than what it can process.
Let’s simulate the Function doing some work by adding a Thread sleep to our code, as shown below.
public string FunctionHandler(string input, ILambdaContext context)
{
Count++;
Thread.Sleep(1000 * 5);
return $"{input} - {Count}";
}
The Function now takes 5 seconds to respond once invoked.
If we make two parallel requests to our Lambda Function, the Lambda Runtime will automatically create 2 instances to serve the requests simultaneously. This behavior changes based on the Lambda concurrency setting.
To simulate two parallel requests, you can make requests from Visual Studio, AWS Console, or the command line.
Below is how you can use the aws cli to invoke a function in PowerShell.
aws lambda invoke --function-name lambda-lifecycle --cli-binary-format raw-in-base64-out --payload '\\"Console\\"' response.json && cat .\\response.json
If you make two parallel calls after freshly deploying the Lambda Function with the above code, you will see that both invocations return the Count as 1. Since Lambda creates multiple instances of the class to handle both requests happening at the same time.
What to Put In The .NET Constructor?
It would be best if you didn’t depend on when and how Lambda decides to create new instances. Your code should work regardless of how the internals of Lambda works.
So to ensure this, make sure your Function classes are stateless, and you don't depend on any particular class level variables within your Function Handler.
This brings us to the obvious question, What can I put in the .NET Constructor?
I typically have any connections to databases or external services, instantiated in the class constructor, and reuse them inside the Function handler, provided these connections are stateless and thread-safe.
Some examples are DynamoDB connections, Parameter Store connections, etc.
For anything else I prefer to have the variables scoped to the Function handler.
Rahul Nath Newsletter
Join the newsletter to receive the latest updates in your inbox.