Using singletons in .NET Core in AWS Lambda

Understanding Dependency Injection with .NET and AWS Lambda

Using singletons in .NET Core in AWS Lambda

Altough most people would agree that generally speaking you should never cache anything inside an AWS Lambda, I still think it's important to sort out how that would work if you were doing it, or if you encounter a codebase that seem to be doing it and you're having problems with it. Since Dependency Injection (DI) containers are very common today (for better or for worse), we will use a singleton service as our base story-line. With that being said, let's go.

If you're like me and are used to long-running processes, it takes some time to get your head around thinking in terms of how AWS Lambda work and specifically the cold- and warm start. In a long-running process your DI container is only re-instantiated or "re-created" if you deploy new code or your application crashes and has to be restarted, but what about AWS Lambda? In this particular post I want to explain how your singleton services will work with cold- and warm start in AWS Lambda. A cruical topic to understand to design your code properly.

Take a look at the below picture, it describes what a cold start versus a warm start is.

From this picture we can see there's some download of code and setup taking place. But what exactly does "Initialization and start of the code" means?

We can see according to the picture it's on the User side (our side) at least, indicating we do have control of it. Does it mean our C# class is instantiated and the constructor is called? Or is that part of Code Execution? Also, since the AWS Lambda is executed in a container, does that mean a warm start starts the container or is the container already started?

Given that our DI container is instantiated in our constructor, this is important to know as it will tell us whether or not our DI container is instantiated only once per cold start or for every warm start too.

According to this SO answer it states that the constructor is indeed only called once, and that is per cold start. From this can we infer that yes indeed, "Initialization and start of the code" means that we have a living C# object in memory, and if you have set up your DI container with singletons in your constructor then every warm-start will indeed reuse these singleton services every single time until your Lambda requires a cold start. Here you can see that code example which explains it pretty well I think:

Update 2021-10-24:
I did some tests and actually one must explicitly create a new scope in our function handle in order for scoped services to be resolved correctly. If you don't do this then multiple requests/calls to your Lambda could get the same scoped service. So below code is an improved version of our function handle that addresses that issue:

So what does all this tells us?

  1. Whenever your Lambda can process a warm start request, your constructor will not run.
  2. You should not setup a DI container in your FunctionHandler method, as it then would be instantiated on every warm start too.
  3. If you wire up your DI container in the constructor, and cache something without an expiration date in a singleton service, it will be cached for all subsequent warm start invocations.
  4. If you wire up your DI container in the constructor, and cache something in a singleton service, your cache will be empty during next cold start. There seem to be no pre-defined threshold of when a cold start occurs, but according to this post the probability is high after 5-7 minutes if receving no traffic.
  5. If you want to cache something for the entire lifetime of only one particular invocation, you cannot use a singleton service, but you could wire up your service as scoped instead.
  6. Your AWS Lambda container (not to confuse with the DI container) is started (and kept running) once per cold start.

Summary

Most explanations on the web about "cold start vs warm start" isn't very C#/.NET specific, and doesn't go into detail about when exactly your C# class is instantiated. My goal with this post was to shine light on that, so you don't get too many surprises when designing your code.