Reducing .NET AWS Lambda Cold Starts by Adding Memory

I was asked to speed up a search feature of a product built almost entirely with AWS Lambda. The application is the result of what was sold internally as a large cloud migration. I wrote some more about this project in the footnotes, as it’s not relevant to the topic at hand1.
The cold start time for this Lambda was ~7 seconds. Quite a long time for the user to wait after clicking a button on a website to trigger it. I tried to devise a way to improve it “properly,” but with all manner of pre-existing technical hurdles1 to overcome, there was no good way to do it.
I didn’t know this at the time, but when provisioning a Lambda, you choose how much memory to allocate rather than explicitly choosing how much CPU it gets. Or to phrase it differently, if you require more CPU, you select more memory.
The Lambdas were assigned 512 MB, which, according to AWS, equates to roughly 0.29 vCPU - less than a third of a vCPU’s compute capacity! I had a vague intuition that giving them a full vCPU would have a significant impact. This is where the 1,769 MB figure comes from: at that memory allocation, Lambda provides the equivalent of 1 vCPU.
You can configure memory between 128 MB and 10,240 MB in 1-MB increments. At 1,769 MB, a function has the equivalent of one vCPU (one vCPU-second of credits per second).
If a function is CPU, network or memory-bound, then increasing the memory setting can dramatically improve its performance.
The table below shows just how much of a limiting factor the assigned memory was. Ironically, this particular Lambda wouldn’t even need 128 MB if memory and compute weren’t coupled. In that sense, allocating more memory feels wasteful, but the shorter execution time actually makes it cheaper overall.
With more memory assigned, cold starts and warm invocations were ~3.2x faster, demonstrating that the Lambda was CPU-starved even when warm. As noted1, pre-existing design problems inflate every figure here.
| Memory | State | Min (ms) | Median (ms) | Max (ms) |
|---|---|---|---|---|
| 512 MB | Cold | 5,089 | 6,569 | 7,990 |
| 512 MB | Warm | 498 | 577 | 2,168 |
| 1769 MB | Cold | 1,777 | 2,049 | 2,757 |
| 1769 MB | Warm | 161 | 181 | 289 |
Footnotes#
-
The migration was poorly planned and repeated many of the same mistakes as the previous system. This is particularly disappointing given the opportunity for a greenfield approach, where the application could have been rebuilt from the ground up. Instead, the legacy code and architecture were transferred into what I can only describe as a soup of Lambdas, with the same poor code quality and lack of unit or integration tests. Additional unnecessary complexity was introduced too, including a source generator used to build a bespoke pseudo-middleware framework resembling ASP.NET Core, despite ASP.NET Core itself being perfectly usable with Lambda. All of this is important context, because improving the feature meant addressing not only Lambda cold starts, but this added overhead too. ↩ ↩2 ↩3