With the release of .NET 8 AWS Lambda now supports .NET 8 and .NET 6 as managed runtimes. With the availability of ARM64 using Graviton2 there have been vast improvements to using .NET with Lambda.
But how does that translate to actual application performance? And how does .NET compare to other available runtimes. This repository contains a simple serverless application across a range of .NET implementations and the corresponding benchmarking results.
The application consists of an Amazon API Gateway backed by four Lambda functions and an Amazon DynamoDB table for storage.
It includes the below implementations as well as benchmarking results for both x86 and ARM64:
- .NET 6 Lambda
- .NET 6 Top Level statements
- .NET 6 Minimal API
- .NET 6 Minimal API with AWS Lambda Web Adapter
- .NET 6 NativeAOT compilation
- .NET 8
- .NET 8 Native AOT
- .NET 8 Minimal API
There are four implementations included in the repository, covering a variety of Lambda runtimes and features. All the implementations use 1024MB of memory with Graviton2 (ARM64) as default. Tests are also executed against x86_64 architectures for comparison.
There is a separate project for each of the four Lambda functions, as well as a shared library that contains the data access implementations. It uses the hexagonal architecture pattern to decouple the entry points, from the main domain and storage logic.
This implementation is the simplest route to upgrade a .NET Core 3.1 function to use .NET 6 as it only requires upgrading the function runtime, project target framework and any dependencies as per the final section of this link.
This implementation uses the new features detailed in this link including
- Top Level Statements
- Source generation
- Executable assemblies
There is a single project named ApiBootstrap that contains all the start-up code and API endpoint mapping. The SAM template still deploys a separate function per API endpoint to negate concurrency issues.
It uses the new minimal API hosting model as detailed here.
Same as minimal API but instead of using Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer it is based on Aws Lambda Web Adapter
The code is compiled natively for either Linux-x86_64 or Linux-ARM64 and then deployed manually to Lambda as a zip file. The SAM deploy can still be used to stand up the API Gateway endpoints and DynamoDb table, but won't be able to deploy native AOT .NET Lambda functions yet. Packages need to be published from Linux, since cross-OS native compilation is not supported yet.
Details for compiling .NET 6 native AOT can be found here
The code is compiled for the .NET 8 AWS Lambda managed runtime. The code is compiled as ReadyToRun for cold start speed. This sample should be able to be tested with sam build
and then sam deploy --guided
.
The code is compiled natively for Linux-x86_64 or ARM64 then deployed to Lambda as a zip file.
Details for compiling .NET native AOT can be found here
There is a single project named ApiBootstrap that contains all the start-up code and API endpoint mapping. The code is compiled natively for Linux-x86_64 then deployed manually to Lambda as a zip file. Microsoft have announced limited support for ASP.NET and native AOT in .NET 8, using the WebApplication.CreateSlimBuilder(args);
method.
Details for compiling .NET 8 native AOT can be found here
To deploy the architecture into your AWS account, navigate into the respective folder under the src folder and run 'sam deploy --guided'. This will launch a deployment wizard, complete the required values to initiate the deployment. For example, for .NET 6:
cd src/NET6
sam build
sam deploy --guided
Benchmarks are executed using Artillery. Artillery is a modern load testing & smoke testing library for SRE and DevOps.
To run the tests, use the below scripts. Replace the $API_URL with the API URL output from the deployment:
cd loadtest
artillery run load-test.yml --target "$API_URL"
Below is the cold start and warm start latencies observed. Please refer to the load test folder to see the specifics of the test that were executed.
All latencies listed below are in milliseconds.
is used to make 100 requests / second for 10 minutes to our API endpoints.
AWS Lambda Power Tuning is used to optimize the cost/performance. 1024MB of function memory provided the optimal balance between cost and performance.
For the .NET 8 Native AOT compiled example the optimal memory allocation was 3008mb.
The below CloudWatch Log Insights query was used to generate the results:
filter @type="REPORT"
| fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldstart
| stats count(*) as count, pct(duration, 50) as p50, pct(duration, 90) as p90, pct(duration, 99) as p99, max(duration) as max by coldstart
Cold Start (ms) | Warm Start (ms) | |||||||
---|---|---|---|---|---|---|---|---|
p50 | p90 | p99 | max | p50 | p90 | p99 | max | |
ARM64 | 873.59 | 909.23 | 944.42 | 945.25 | 5.50 | 9.24 | 19.53 | 421.72 |
X86 | 778.74 | 966.39 | 1470.50 | 1659.51 | 6.41 | 11.90 | 31.33 | 255.98 |
x86 with Powertools | 855.45 | 915.61 | 1031.25 | 1381.09 | 5.82 | 9.83 | 27.59 | 748.08 |
Container Image on X86 | 980.98 | 1256.94 | 1532.01 | 1755.68 | 5.82 | 9.84 | 24.42 | 260.25 |
ARM64 with top level statements | 916.53 | 955.82 | 985.90 | 1021.40 | 5.73 | 9.38 | 20.65 | 417.23 |
Minimal API on x86 | 1742.83 | 1966.88 | 2411.74 | 2503.31 | 5.91 | 9.99 | 21.74 | 108.6 |
Minimal API on ARM64 | 2105.21 | 2164.96 | 2215.31 | 2228.18 | 6.20 | 9.67 | 20.08 | 528.13 |
Minimal API with aws lambda web adapter on x86 | 1013.88 | 1102.67 | 1330.62 | 1392.85 | 6.20 | 10.31 | 21.74 | 154.62 |
Minimal API with aws lambda web adapter on ARM64 | 1335.57 | 1395.04 | 1455.09 | 1455.09 | 7.04 | 15.58 | 36.71 | 111.28 |
Native AOT on ARM64 | 1277.19 | 1326.64 | 1358.84 | 1367.49 | 6.10 | 9.37 | 17.97 | 838.78 |
Native AOT on X86 | 466.81 | 542.86 | 700.45 | 730.51 | 6.21 | 11.34 | 24.69 | 371.16 |
The .NET 8 benchmarks include the number of cold and warm starts, alongside the performance numbers. Typically, the cold starts account for 1% or less of the total number of invocations.
Cold Start (ms) | Warm Start (ms) | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
Invoke Count | p50 | p90 | p99 | max | Invoke Count | p50 | p90 | p99 | max | |
x86_64 | 1490 | 860 | 962 | 1403 | 1676 | 45,436 | 6.1 | 10.7 | 27.7 | 63.4 |
ARM64 | 1699 | 1063 | 1112 | 1155 | 1209 | 45,093 | 6.6 | 14.6 | 30.8 | 75.9 |
x86_64 Native AOT | 758 | 322 | 344 | 441 | 665 | 45,914 | 5.0 | 7.7 | 14.7 | 77.0 |
ARM64 Native AOT | 689 | 334 | 347 | 372 | 442 | 646,081 | 5.3 | 7.9 | 13.4 | 54.6 |
ARM64 Native AOT Minimal API | 91 | 498 | 522 | 895 | 895 | 156,359 | 5.6 | 8.8 | 16.1 | 214.3 |
*Microsoft do not officially support all ASP.NET Core features for native AOT, some features of ASP.NET may not be supported.
Native AOT container samples use an Alpine base image. A cold start latency of ~1s was seen the first time an image was pushed and invoked.
On future invokes, even after forcing new Lambda execution environments, cold start latency is as seen above. Potential reasons why covered in an AWS blog post on optimizing Lambda functions packaged as containers.
You can find implementations of this project in other languages here:
See CONTRIBUTING for more information.
This library is licensed under the MIT-0 License. See the LICENSE file.