Building Fast Serverless APIs With Minimal APIs on AWS Lambda
Learn to run ASP.NET Core Minimal APIs on AWS Lambda for fast, cheap serverless APIs. Covers setup, cold starts, Native AOT, and .NET 10 with diagrams and code.
A taxi you call instead of a car you own
Think about two ways to get around your city.
The first way is to buy your own car. It sits in front of your house all day and night. It is ready the moment you need it, but you pay for it even while it just sits there — the loan, the parking, the insurance, the petrol. On a quiet week when you barely go out, you are still paying for a car doing nothing.
The second way is to call an auto-rickshaw or a taxi only when you actually need to go somewhere. When you step out of the ride, you stop paying. There is no car parked outside eating your money on lazy days. The small catch is that the taxi takes a minute to arrive. If you are in a real hurry and none is nearby, you wait a little.
A normal web server is the car you own. It runs all day, ready for requests, and you pay for every hour even at 3 a.m. when nobody is using your app.
AWS Lambda is the taxi. Your code only runs when a request actually arrives, and you only pay for the milliseconds it runs. The "minute the taxi takes to arrive" is the famous cold start. In this post we will build a fast serverless API using ASP.NET Core Minimal APIs on Lambda, and we will spend real effort making that taxi arrive as quickly as possible.
What we are building
We will take an ordinary Minimal API — the same kind you would run anywhere — and make it run on Lambda with almost no changes. Then we will tune it.
The key idea: your ASP.NET Core code does not know it is on Lambda. It just sees normal HTTP requests. A thin layer translates the Lambda event into a request your endpoints already understand.
Step 1: A plain Minimal API
Let us start with code you have probably seen before. Nothing special, nothing serverless yet.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IProductStore, ProductStore>();
var app = builder.Build();
app.MapGet("/products", (IProductStore store) =>
Results.Ok(store.GetAll()));
app.MapGet("/products/{id:int}", (int id, IProductStore store) =>
store.Find(id) is { } product
? Results.Ok(product)
: Results.NotFound());
app.MapPost("/products", (Product product, IProductStore store) =>
{
store.Add(product);
return Results.Created($"/products/{product.Id}", product);
});
app.Run();This runs happily on your laptop with dotnet run. It uses the Kestrel web server, which is the normal engine inside every ASP.NET Core app. Our job now is to let the very same code run inside Lambda.
Step 2: Add Lambda hosting (one package, one line)
Install the hosting package:
dotnet add package Amazon.Lambda.AspNetCoreServer.HostingThen add a single line to Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IProductStore, ProductStore>();
// This is the only new line. It lets the app run on Lambda.
builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
var app = builder.Build();
app.MapGet("/products", (IProductStore store) =>
Results.Ok(store.GetAll()));
// ... the rest of your endpoints are unchanged ...
app.Run();That is the whole trick. Here is the clever part: AddAWSLambdaHosting looks around and asks, "Am I actually running inside Lambda right now?" If yes, it swaps Kestrel for a Lambda-aware server. If no — like when you run on your own machine — it does nothing and Kestrel stays. So local debugging feels exactly the same as before.
From laptop to Lambda
Steps
Write API
Normal Minimal API
Add hosting line
AddAWSLambdaHosting
Run locally
Uses Kestrel as usual
Deploy to Lambda
Swaps to Lambda server
The LambdaEventSource you pass tells the framework what kind of front door sits in front of your function. The three common choices are below.
| Event source | When to use it | Notes |
|---|---|---|
HttpApi | Most new REST APIs | Cheapest and fastest API Gateway option |
RestApi | When you need API keys, usage plans, request validation | Older, more features, costs a bit more |
ApplicationLoadBalancer | When traffic comes through an ALB | Good for VPC-internal apps |
For most people starting out, HttpApi is the right answer. It is the leanest and least expensive way to get HTTP into Lambda.
Step 3: How a request actually flows
It helps to picture exactly what happens when someone calls your API. Nothing here is magic — it is just translation.
API Gateway receives the real HTTP request and wraps it in a JSON event. Lambda hands that event to the hosting package, which unwraps it back into a request object your endpoint understands. Your endpoint runs as normal and returns a response. The package wraps the response back into JSON, and API Gateway sends it to the user. Your MapGet code never knew any of this happened.
Step 4: Deploying it
You package the function and ship it. The Amazon Lambda Tools for .NET make this a one-command job. First install them once:
dotnet tool install -g Amazon.Lambda.ToolsAdd an aws-lambda-tools-defaults.json file so you do not have to type settings every time, then deploy:
dotnet lambda deploy-function products-apiBehind the scenes this builds your project, zips it, uploads it, and wires it to Lambda. There are other paths too — the AWS SAM CLI, the AWS CDK, Terraform, or a container image — but the .NET tool is the gentlest place to begin.
A few limits worth keeping in your head from day one:
| Limit | Value | Why it matters |
|---|---|---|
| Zipped package size | 50 MB | Keep dependencies lean |
| Unzipped package size | 250 MB | Trimming helps a lot |
| Max run time | 15 minutes | Lambda is not for long jobs |
| Default memory | 128 MB and up | More memory also means more CPU |
That last row hides a useful secret: on Lambda, memory and CPU are linked. Giving your function more memory also gives it more processing power, which can make cold starts shorter. Sometimes paying for more memory makes each request cheaper overall because it finishes faster. It is worth measuring.
The cold start problem, honestly
Let us not pretend. The taxi takes a moment to arrive, and we should understand why.
When a request hits a function that has been asleep, Lambda must:
What happens during a cold start
Steps
Find machine
Allocate compute
Download code
Pull your package
Start runtime
.NET runtime boots
Build app
DI and startup run
Serve request
Finally responds
After this, the machine stays warm for a while. The next requests skip every step and answer in a few milliseconds. So cold starts only sting the first user after a quiet spell. Whether that matters depends on your app. An internal report tool? Nobody cares about 400 extra milliseconds once an hour. A public checkout page? You will want to fight for every millisecond.
Here is how the speeds roughly compare, so you can set expectations.
Making the taxi arrive faster
Four levers do most of the work. You can pull them one at a time and measure each.
Lever 1: Move to .NET 10
.NET 10 is the current LTS release, and the .NET 10 runtime is available on Lambda. The team put real effort into faster startup. Just moving from .NET 8 to .NET 10 can cut cold start time sharply with zero code changes — published comparisons show the newer runtime starting many times faster than .NET 8 in some cases. This is the cheapest win available: change your target framework and the Lambda runtime, then redeploy.
Lever 2: Use ARM (Graviton)
Lambda can run your function on Amazon's own ARM-based Graviton processors instead of x86. Graviton is usually both cheaper and a little faster for .NET workloads. You pick it with a single setting in your deployment config (Architectures: arm64). It is close to free money — measure, and if your numbers look good, keep it.
Lever 3: Trim your package
Smaller packages download faster, so they cold-start faster. Trimming removes code your app never calls. Keep your dependency list short. Avoid pulling in giant libraries for tiny features. Remember those 50 MB and 250 MB limits — staying well under them is good for both speed and safety.
Lever 4: Native AOT, the big one
This is the heavyweight. Native AOT compiles your app ahead of time into a plain native program. Lambda no longer has to start the .NET runtime or compile any code while a user waits — it just runs a finished executable. The results are large: published reports show cold start drops of roughly 75% to 85%, and meaningfully lower bills because each cold request is billed for less time.
There is a catch, and it is important. Native AOT trims aggressively and bans most runtime reflection. JSON serialization through reflection will not work. You must switch to source-generated serialization, where the compiler writes the serialization code for you at build time.
using System.Text.Json.Serialization;
// Tell the compiler exactly which types to generate
// serialization code for. No reflection at runtime.
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(Product[]))]
internal partial class ApiJsonContext : JsonSerializerContext
{
}
// Then point your app at that context.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(
0, ApiJsonContext.Default);
});If your app and its libraries are AOT-friendly, this lever is the single biggest cold start improvement you can make. If you depend on libraries that still need reflection and are not AOT-ready, stay on the regular runtime and lean on levers 1 to 3 instead.
A quick note on libraries while we are here: some popular .NET tools changed their licensing recently. MediatR and MassTransit are now commercially licensed for many uses. They are not required for Lambda at all, but if you reach for them out of habit, check the license terms for your project before shipping. For a small serverless API you often do not need them — plain Minimal API endpoints are enough.
Keeping it warm (when you must)
If cold starts truly hurt your users, Lambda offers provisioned concurrency. You tell Lambda to keep a set number of machines pre-warmed and ready. Those requests never pay the cold start cost. The trade-off is that you now pay to keep them ready — a little like keeping one taxi permanently parked outside. Use it only when the user experience clearly needs it, because it eats into the "pay nothing when idle" benefit that made Lambda attractive in the first place.
Choosing your cold start strategy
Steps
Upgrade .NET
Free, no code change
Try Graviton
Cheaper and faster
Trim package
Smaller is faster
Native AOT
Biggest win, some limits
Provisioned concurrency
Last resort, costs more
Is Lambda the right home for your API?
Lambda is wonderful, but it is not for everything. Use this simple guide.
Lambda is a great fit when:
- Traffic is bursty or unpredictable — quiet for hours, then busy for minutes.
- The workload is small: webhooks, internal tools, light back-office APIs.
- You hate managing servers and want to pay only for what you use.
- Each request finishes quickly, well under the 15-minute limit.
Lambda is a poor fit when:
- You need guaranteed sub-100-millisecond responses at all times.
- Traffic is heavy and steady all day, where an always-on container can be cheaper.
- You have long-running jobs that exceed 15 minutes.
- Your deployment cannot be made to fit the package size limits.
The honest summary: Lambda shines for small, event-driven, or intermittent work that does not justify a full-time server. For a constantly hammered, latency-critical service, a traditional host or container may serve you better. There is no shame in either choice — pick the taxi or the car based on how you actually travel.
A mental model to remember
Keep this picture in your head. Most small APIs land on "Use Lambda," and that is exactly where serverless gives you the most joy for the least effort and cost.
Putting it all together
Here is the shape of a tidy, Lambda-ready Program.cs that uses the ideas above.
var builder = WebApplication.CreateBuilder(args);
// App services
builder.Services.AddSingleton<IProductStore, ProductStore>();
// Lambda hosting — only activates when running inside Lambda
builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
// Source-generated JSON for fast, AOT-friendly serialization
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(
0, ApiJsonContext.Default);
});
var app = builder.Build();
app.MapGet("/products", (IProductStore store) =>
Results.Ok(store.GetAll()));
app.MapGet("/products/{id:int}", (int id, IProductStore store) =>
store.Find(id) is { } product
? Results.Ok(product)
: Results.NotFound());
app.MapPost("/products", (Product product, IProductStore store) =>
{
store.Add(product);
return Results.Created($"/products/{product.Id}", product);
});
app.Run();Notice how little of this is about Lambda. The endpoints are pure ASP.NET Core. Two lines — the hosting line and the JSON line — carry all the serverless wiring. That is the beauty of the approach: you keep writing normal APIs, and the cloud-specific parts stay tiny and out of your way.
References and further reading
- Deploy ASP.NET applications — AWS Lambda documentation
- Compile .NET Lambda function code to a native runtime (Native AOT) — AWS Lambda documentation
- .NET 10 runtime now available in AWS Lambda — AWS Compute Blog
- Running Serverless ASP.NET Core Web APIs with Amazon Lambda — AWS Developer Tools Blog
- Building Fast Serverless APIs With Minimal APIs on AWS Lambda — Milan Jovanović
- Minimal APIs overview — Microsoft Learn
Quick recap
- AWS Lambda is like a taxi: your code runs only when a request arrives, and you pay only for those milliseconds — no always-on server bill.
- Almost no rewrite needed: add the
Amazon.Lambda.AspNetCoreServer.Hostingpackage and oneAddAWSLambdaHostingline. Your endpoints stay normal. - Local feels the same: Kestrel runs on your machine; the Lambda server only takes over inside Lambda.
- Cold start is the catch: the first request after a quiet spell is slower because Lambda must start your runtime and app.
- Four cold start levers: upgrade to .NET 10, run on ARM Graviton, trim your package, and use Native AOT (with source-generated JSON) for the biggest win.
- Native AOT bans reflection: you must switch to source-generated serialization and test carefully.
- Provisioned concurrency keeps machines warm when users truly cannot tolerate cold starts, but it costs more.
- Pick the right home: Lambda loves small, bursty, short workloads; busy, steady, latency-critical APIs may prefer a container or traditional host.
- Mind the limits: 50 MB zipped, 250 MB unzipped, 15-minute max run time. More memory also buys more CPU.
Related Posts
Automatically Register Minimal APIs in ASP.NET Core
Learn to auto-register Minimal API endpoints in ASP.NET Core using the IEndpoint pattern, assembly scanning, and source generators. With diagrams and code.
Best Practices for Building REST APIs in ASP.NET Core
A friendly, beginner guide to REST API best practices in ASP.NET Core with naming, status codes, validation, ProblemDetails, paging, versioning, security, and code.
Caching in ASP.NET Core: Make Your App Fast (The Easy Way)
Learn caching in ASP.NET Core with simple examples. Understand in-memory cache, distributed Redis cache, HybridCache, and output cache, with diagrams, code, and clear advice on which to use and when.
How to Structure Minimal APIs in ASP.NET Core (.NET 10)
Learn how to structure Minimal APIs in ASP.NET Core with route groups, endpoint files, DTOs, TypedResults, and filters. Beginner-friendly with diagrams.
How to Be More Productive When Creating CRUD APIs in .NET
Learn simple, modern ways to build CRUD APIs faster in .NET 10. Scaffolding, minimal APIs, EF Core, DTO mapping, and reusable patterns explained for beginners.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.