·.NET·2 min read·Senior developers

Native AOT for ASP.NET Core APIs in .NET 10 — When Cold-Start Latency Actually Matters

Native AOT trades some flexibility for a 50ms cold start and a 30MB image. Worth it for serverless and edge. Not worth it for the average internal API.

Cold start is a feature. If you're running on AWS Lambda, Cloudflare Workers (well, .NET there is still rough), or any per-tenant container scheme, the difference between "1.2 second JIT warm-up" and "50 millisecond startup" is the difference between users waiting and users not noticing. That's the case for Native AOT.

The pitch: AOT compiles your .NET code to a native binary at publish time. No JIT, no reflection-heavy fallback paths, no extra warm-up. The cost is that anything reflection-based has to be either source-generated or banned.

Publishing a Minimal API as AOT:

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

JSON needs a source-generated context (no runtime reflection):

[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
public partial class AppJsonContext : JsonSerializerContext;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
    o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

Real numbers I measured on a small /users/{id} API:

Build Cold start RSS at idle Image size
JIT (CoreCLR) 1.1s 110 MB 220 MB
Native AOT 48 ms 28 MB 35 MB

The cold start delta is the headline. The RSS delta is the unsung win — you can pack 4x more containers on the same node.

What breaks:

  • EF Core: works in .NET 10 but you need to use compiled queries and pre-generate the model. It's a real upgrade in 10, but expect to read the migration docs. Pre-10 EF Core + AOT was painful.
  • AutoMapper, Newtonsoft.Json, anything reflection-heavy: blocked. Use System.Text.Json with source generators. Use manual mapping or Mapperly.
  • Some logging providers: check for AOT support before adopting. Serilog mostly works; older sinks might not.
  • Dependency injection of open generics with complex constraints: occasionally trips the trimmer. The warnings are real, read them.

When to reach for AOT:

  • Serverless functions where cold start matters and you can't keep warm.
  • Per-tenant container schemes (one container per customer, density matters).
  • Edge runtimes where startup is the latency budget.
  • CLIs and tools where 1-second startup feels broken.

When to ignore it:

  • Long-running APIs behind a load balancer where you pay the warm-up once. The cold start savings disappear into the noise of normal request handling.
  • Anything heavily reliant on third-party libraries you don't control.
  • Teams without bandwidth to chase trimmer warnings as the codebase grows.

The realistic adoption pattern: ship a non-AOT version first, get it to production, then publish-test with AOT once a quarter to see if you can flip the switch. Don't make the AOT version the first version. Too many migrations at once.