I care a lot about performance. Making my code faster makes me happy and the metric for success is trivial. It's especially trivial in AWS Lambda since execution time is automatically reported. There's also a financial incentive as it is tied to the billing model.
And so, I've been wondering: Is there an optimal strategy for my .NET code on AWS Lambda?
AWS Lambda is its own little beast to understand and master. There are a variety of deployment options available that impact performance. Combined with the various compilation and runtime options of .NET code, it presented an interesting challenge. There is also the fundamental question of what does "optimal" mean?
Well ⟪spoiler alert⟫ there isn't just one "optimal" strategy! Instead, we have to choose what we want to optimize for. I settled on examining two strategies. Both have their place and it's rarely (never?) possible to achieve both. Note that if some of the terminology is unfamiliar, fear not. I'm going to explain each concept as we dive into the material. And, if I miss something, please let me know in the comments and I'll amend the posts.
This strategy seeks to achieve the shortest response time in the worst-case scenario: a cold start of our AWS Lambda function. In this case, a new execution environment must be created to handle our request. The execution environment has to be initialized, our code has to be loaded into it, and then our logic needs to be run to produce a response. All of this takes additional time when compared to subsequent requests. Correspondingly, this strategy trades faster initialization for absolute performance in later invocations, which may impact execution cost.
This strategy minimizes the execution cost of the AWS Lambda instance, including cold start followed by some threshold of warm invocations. In this case, we are willing to have slower cold starts for better performance once the everything is warmed up. This strategy leverages one of the lesser-known quirks of the AWS Lambda billing model works (more on this later). This strategy works best when we know that our code will be invoked frequently. For the purpose of this study, I assume that our Lambda function is invoked at 100 times after a cold start.
The preferred strategy depends on the purpose of our code. For handling synchronous invocations, such as API requests, minimizing cold starts can be preferrable. Especially when a human is waiting at the other end. But for asynchronous invocations, such as with EventBridge, then minimizing execution cost is more important.
In the next post, I'm going to dive into the lifecycle of an AWS Lambda instance and associated terminology. I'll then cover some of the compilation and runtime options for .NET code. Then I'll introduce the benchmarking methodology. Finally, I'll present my findings and conclusion. As with all studies, peer review and independent confirmation is critical. Therefore, all my code is also made available under a permissive open-source license in the LambdaSharp.Benchmark GitHub repository.
Disclaimer: This is stating the obvious, but please check the date of this post. If it's older than 3 years, it's probably out-of-date!