As of yesterday, AWS announced support for the .NET Core 3.1 runtime on AWS Lambda. This has been an eagerly anticipated upgrade as there is reason to believe that the long-standing and well documented issues with cold starts should be somewhat mitigated.

This optimism comes from the introduction of the ReadyToRun compilation option in .NET Core 3.0 which allows your code to be pre-compiled for a specific target runtime, reducing the amount of just-in-time (JIT) compilation required on program startup. The trade-off is that the binary will be larger in size and not cross-platform compatible, but for .NET Lambda deployables specifically I think most will agree this is an acceptable one to make. In addition, the new performanced-focused System.Text.Json library introduced in .NET 3.1 should also help somewhat with initialization by making it quicker to deserialize that Lambda event payload, though I imagine the impacts from this may vary quite a bit depending on the use case.

I ran some quick before/after experiments using a dummy Application Load Balancer function I already had that targeted .NET Core 2.1. I upgraded this Lambda using the instructions in the announcement blog post by simply having it target 3.1 and using the new Amazon.Lambda.Serialization.SystemTextJson instead of the old one based on Newtonsoft.Json. I then packaged it using ReadyToRun by using a Docker image with the 3.1 SDK on it (instructions below), uploaded the new .zip to Lambda and changed the runtime to 3.1. Everything worked immediately with no hiccups on an existing function which was an added bonus.

If you look at the code, you may think that this function doesn’t really do anything, well that’s kind of the point. We are only interested in the base overhead that the .NET Core Lambda runtime provides, so we can evaluate it’s feasibility for specific use cases. One thing I’m not really showing here is the impact of ReadyToRun on a Lambda that pulls in a lot of depedencies, I will leave that for another future round of testing as I wanted to get a quick “first-take” with this just being announced yesterday.

Results

Old 2.1 ALB function

Memory Test Run #1 (ms) Test Run #2 (ms) Test Run #3 (ms)
128mb 3214.66 3266.76 3204.84
256mb 1416.55 1564.59 1479.23
512mb 704.10 712.95 736.18
1024mb 355.97 356.11 352.68
2048mb 169.25 205.09 204.44

New 3.1 ALB function (Linux ReadyToRun + System.Text.Json)

Memory Test Run #1 (ms) Test Run #2 (ms) Test Run #3 (ms)
128mb 881.83 905.15 929.12
256mb 422.52 431.35 440.88
512mb 187.82 195.23 192.24
1024mb 97.71 96.34 93.74
2048mb 64.10 62.64 59.46

Verdict

These are obviously really impressive improvements. While it is still relatively “unusable” at 128-256mb for latency sensitive workloads, the overhead from 512mb and above is quite acceptable and does now make this platform (and the entire serverless paradigm) much more attractive in my opinion.

Preparing a Linux optimized Lambda using ReadyToRun and Docker

First create this simple Dockerfile with the 3.1 SDK on it:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1
COPY . /app
WORKDIR /app

Now create an image, then start a container and run a shell on it:

docker build --tag <imageName> .
docker run --entrypoint "/bin/sh" -it <imageName>

First we’ll need to install a few pre-requisites:

apt update
apt install zip
dotnet tool install -g Amazon.Lambda.Tools
export PATH="$PATH:/root/.dotnet/tools"

We can then use the Amazon.Lambda.Tools to package up the Lambda into a .zip ready to upload:

cd src/lambda21test
dotnet lambda package --msbuild-parameters "/p:PublishReadyToRun=true --self-contained false"

Lastly, we need to copy the .zip out of the Docker container onto our machines so we can upload it to Lambda via the AWS Console:

exit
docker start <containerId>
docker cp <containerId>:/app/src/lambda21test/bin/Release/netcoreapp3.1/lambda21test.zip lambda21test.zip

Apologies if any of that is clunky, I am not a Docker expert by any means.