With the release of .NET 6 AWS Lambda now supports .NET Core 3.1 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 Core 3.1
- .NET Core 3.1 with Open Telemetry tracing
- .NET 6 Lambda
- .NET 6 Top Level statements
- .NET 6 Minimal API
- .NET 6 NativeAOT compilation
- .NET 7 NativeAOT compilation
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
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 NativeAOT .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 NativeAOT can be found here
The code is compiled natively for Linux-x86_64 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 NativeAOT .NET Lambda functions yet. Packages need to be published from Linux, since cross-OS native compilation is not supported yet.
Details for compiling .NET 7 NativeAOT can be found here
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.
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.
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 | |
.NET Core 3.1 (arm64) | 1122.70 | 1170.83 | 1225.92 | 1326.32 | 5.55 | 8.74 | 19.85 | 256.55 |
.NET Core 3.1 (x86_64) | 1004.80 | 1135.81 | 1422.78 | 1786.78 | 6.11 | 10.82 | 29.40 | 247.32 |
.NET Core 3.1 with Open Telemetry (x86_64) | 1615.31 | 1704.93 | 1931.82 | 2067.97 | 7.04 | 12.08 | 35.57 | 1059.78 |
NET 6 (arm64) | 873.59 | 909.23 | 944.42 | 945.25 | 5.50 | 9.24 | 19.53 | 421.72 |
NET 6 with Lambda Powertools v0.0.1 (arm64) | 981.97 | 1031.25 | 1062.65 | 1091.56 | 6.72 | 13.76 | 35.79 | 70.58 |
NET 6 (x86_64) | 778.74 | 966.39 | 1470.50 | 1659.51 | 6.41 | 11.90 | 31.33 | 255.98 |
NET 6 Top Level Statements (arm64) | 916.53 | 955.82 | 985.90 | 1021.40 | 5.73 | 9.38 | 20.65 | 417.23 |
Minimal API (arm64) | 1149.95 | 1194.47 | 1239.47 | 1315.07 | 6.10 | 10.00 | 22.91 | 1315.07 |
NET 6 NativeAOT (arm64) | 448.97 | 467.75 | 493.20 | 516.6 | 6.30 | 10.49 | 21.50 | 461.35 |
NET 6 NativeAOT (x86_64) | 466.81 | 542.86 | 700.45 | 730.51 | 6.21 | 11.34 | 24.69 | 371.16 |
NET 7 NativeAOT (x86_64) | 381.64 | 411.94 | 411.94 | 411.94 | 5.64 | 9.99 | 22.81 | 245.61 |
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.