Data Layer at Scale — series:

  1. EF Core 10 AOT: what's broken (link TBD)

  2. Dapper.AOT broken on .NET 10 (link TBD)

  3. Extern alias benchmarking (link TBD)

  4. Cold start vs density (this article)

  5. Allocation numbers for k8s (link TBD)

  6. The benchmark lied (link TBD)

  7. The data layer as a line item (link TBD)

  8. Measure, then migrate (link TBD)

If you search for NativeAOT benchmarks, you will find plenty of posts showing dramatic throughput gains and binary-size wins. What you will not find as often is an honest look at cold-start latency — the metric most developers cite when they ask "should I use NativeAOT?"

I ran these measurements while researching the data layer for an enterprise .NET framework that targets Kubernetes-dense deployments. The results were not what I expected — twice.

The Assumption

The conventional wisdom: JIT compilation happens at runtime, so there's inherent warmup latency. NativeAOT compiles everything ahead of time. Therefore NativeAOT cold start should be dramatically faster.

Sounds airtight. It's mostly wrong for typical ASP.NET Core services.

The Setup

The Numbers

ConfigurationBinary SizeCold StartIdle RSSRAM-fit replicas*
ADO.NET — JIT self-contained108 MB~601 ms49 MB155
ADO.NET — AOT native13 MB~530 ms23 MB335
Dapper — JIT self-contained108 MB~590 ms51 MB150
EF Core — JIT self-contained115 MB~1,279 ms77 MB99

floor((8192 − 512) / RSS) on a 4 vCPU / 8 GB node, computed from unrounded measured RSS. This is the RAM ceiling, not a serving-capacity claim — nobody runs 335 pods on 4 vCPUs; the scheduler's per-pod CPU requests bind the count long before RAM does (at a typical 50-millicore request, 80 pods — for every provider alike). What the column actually tells you is whether RAM or CPU is your binding constraint: at 77 MB it's RAM for any request under ~40m; at 23 MB, RAM effectively leaves the constraint set.

Surprise #1: The Cold-Start Gap Is ~70 ms

The AOT binary shaves roughly 70 ms off cold start versus its JIT twin. For most deployments that's noise. Why so small?

For a small-to-medium service, cold start is not dominated by JIT compilation. It's dominated by work that exists identically in both modes:

  1. DI container construction — registrations, graph validation, root ServiceProvider

  2. Middleware pipeline assembly — routing tables, endpoint metadata

  3. Database initialization — connection, journal mode, schema checks

  4. Hosted services and configuration binding

AOT eliminates runtime bring-up almost entirely — but that was the small slice.

The real cold-start villain is EF Core's startup overhead — model-building chiefly: walking entity types, building the internal model, compiling query pipelines adds ~680 ms on top of everything, JIT or not. If cold start matters to you, your ORM choice moves the needle ~10× more than your compilation mode. (EF's compiled-model feature targets exactly this — a separate optimization with its own constraints.)

Does this scale to a bigger app? These numbers are from a deliberately small service, so read the ~70 ms as what we measured here, not a universal law. A larger app JIT-compiles more startup code and loads more assemblies, so AOT's absolute cold-start saving would likely grow. Two things temper that, though: the init costs above (DI, DB, config, EF model) scale right along with the app and AOT does not remove them, and ReadyToRun (PublishReadyToRun=true) recovers most of the startup-JIT time without AOT's trimming constraints. So expect "somewhat more," not automatically "dramatically faster." If cold start is your deciding factor — or your app is reflection-heavy, serialization-heavy, or has a large dependency graph — measure it on your own service (and try R2R before reaching for AOT).

Surprise #2: Where AOT Actually Pays

Look at the last two columns again — then look at what happened under sustained load (8 workers × 60 s, the app's own runtime counters):

ADO.NETCPU cores usedreq/sRSS peakGen0 GCsGC pause total
JIT0.672,36872 MB375364 ms
AOT0.502,58037 MB193231 ms

Same source code. Under load, AOT cut CPU by 25%, RSS by 49%, and Gen0 collections by 49% — while serving slightly more requests. The micro-benchmark view ("AOT is a size/memory play") undersells it: process-level JIT activity, code-heap memory, and the GC pressure they feed are real CPU costs that only show up under sustained traffic.

So the honest mental model:

A Practical Decision Guide

Choose NativeAOT when:

Stick with JIT self-contained when:

For engineering leaders: AOT's ROI shows up on the node bill, not the latency dashboard. Cutting CPU 25% cuts the core count you pay for, and halving RSS means RAM never caps your bin-packing before your CPU budget does. But the prerequisite is a data layer that's AOT-clean — which today is an architectural decision, not a compiler flag.

Conclusion

NativeAOT is a genuine win — just not where most people look for it. Cold start improves by ~70 ms on a lean stack and not at all where EF model-building dominates. The wins that survive contact with production are binary size, idle RSS, and CPU under load — a cost story, and a strong one.

Benchmark your actual service, under actual load, with in-app counters — not just a micro-benchmark. Ours changed our conclusions twice.

Measured on .NET 10 (net10.0), Microsoft.Data.Sqlite / EFCore.Sqlite 10.0.9, Dapper 2.1.66, single x64 host; medians of repeated runs; load metrics self-reported via Environment.CpuUsage and GC counters.

Part of the Data Layer at Scale series — research notes from building Turboservices, a .NET framework for rebuilding legacy systems onto AI-native delivery rails.

About the author — Vitalii Honcharuk is a hands-on Distinguished Engineer, Architect and CTO with 15+ years of experience building frameworks and high-reliability backend systems for enterprises and startups. His current work, Turboservices, turns engineering discipline into compile-time guarantees: unsafe states refuse to build, quality gates are code, and every architectural decision ships with measured evidence.