Skip to main content
SEMastery
ASP.NETintermediate

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.

14 min readUpdated March 13, 2026

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.

A request travels from the internet to your Minimal API code running inside Lambda.

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.Hosting

Then 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

Write API
Add hosting line
Run locally
Deploy to Lambda

Steps

1

Write API

Normal Minimal API

2

Add hosting line

AddAWSLambdaHosting

3

Run locally

Uses Kestrel as usual

4

Deploy to Lambda

Swaps to Lambda server

The same project behaves differently depending on where it runs, with no code changes.

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 sourceWhen to use itNotes
HttpApiMost new REST APIsCheapest and fastest API Gateway option
RestApiWhen you need API keys, usage plans, request validationOlder, more features, costs a bit more
ApplicationLoadBalancerWhen traffic comes through an ALBGood 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.

The journey of one request through Lambda and back, step by step.

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.Tools

Add an aws-lambda-tools-defaults.json file so you do not have to type settings every time, then deploy:

dotnet lambda deploy-function products-api

Behind 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:

LimitValueWhy it matters
Zipped package size50 MBKeep dependencies lean
Unzipped package size250 MBTrimming helps a lot
Max run time15 minutesLambda is not for long jobs
Default memory128 MB and upMore 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

Find machine
Download code
Start runtime
Build app
Serve request

Steps

1

Find machine

Allocate compute

2

Download code

Pull your package

3

Start runtime

.NET runtime boots

4

Build app

DI and startup run

5

Serve request

Finally responds

Every step here adds time before your first response. Warm requests skip all of it.

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.

Relative cold start feel across common .NET Lambda setups, fastest at the bottom.

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

Upgrade .NET
Try Graviton
Trim package
Native AOT
Provisioned concurrency

Steps

1

Upgrade .NET

Free, no code change

2

Try Graviton

Cheaper and faster

3

Trim package

Smaller is faster

4

Native AOT

Biggest win, some limits

5

Provisioned concurrency

Last resort, costs more

Work down the list. Stop when your numbers are good enough.

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

A simple decision flow for whether to put an API on Lambda.

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

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.Hosting package and one AddAWSLambdaHosting line. 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