Data Layer at Scale — series:
EF Core 10 AOT: what's broken (link TBD)
Dapper.AOT broken on .NET 10 (link TBD)
Extern alias benchmarking (link TBD)
Cold start vs density (this article)
Allocation numbers for k8s (link TBD)
The benchmark lied (link TBD)
The data layer as a line item (link TBD)
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
.NET 10 (net10.0), x64, local 4 vCPU / 8 GB Windows workstation — read the numbers as relative comparisons, not absolute SLAs
ASP.NET Core minimal API service — a small CRUD app (one Order domain, ~5 handlers: create, read-by-id, list, delete, plus a transactional outbox), with DI, routing, JSON and DB init. Deliberately lean, to isolate the provider — so the JIT-able startup surface is intentionally small (this matters for the cold-start reading below).
SQLite via three providers: raw ADO.NET, Dapper, EF Core
Cold start = process launch → first HTTP 200, tight polling loop from a sidecar; medians of repeated runs
Publish: self-contained for JIT,
PublishAotfor native
The Numbers
| Configuration | Binary Size | Cold Start | Idle RSS | RAM-fit replicas* |
|---|---|---|---|---|
| ADO.NET — JIT self-contained | 108 MB | ~601 ms | 49 MB | 155 |
| ADO.NET — AOT native | 13 MB | ~530 ms | 23 MB | 335 |
| Dapper — JIT self-contained | 108 MB | ~590 ms | 51 MB | 150 |
| EF Core — JIT self-contained | 115 MB | ~1,279 ms | 77 MB | 99 |
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:
DI container construction — registrations, graph validation, root
ServiceProviderMiddleware pipeline assembly — routing tables, endpoint metadata
Database initialization — connection, journal mode, schema checks
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.NET | CPU cores used | req/s | RSS peak | Gen0 GCs | GC pause total |
|---|---|---|---|---|---|
| JIT | 0.67 | 2,368 | 72 MB | 375 | 364 ms |
| AOT | 0.50 | 2,580 | 37 MB | 193 | 231 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:
Not a cold-start optimization (~70 ms).
A per-core-throughput and density optimization: −25% CPU under load, 8× smaller image (pull times, layer cache, rollout I/O), and an RSS low enough that RAM stops being a bin-packing constraint at all (see the replica-fit note under the table).
A Practical Decision Guide
Choose NativeAOT when:
Pod density or total cluster cost is the constraint
Sustained CPU per request matters (it's your infra bill)
Your dependency stack is AOT-clean — raw ADO.NET is; EF Core and Dapper.AOT currently are not (see the companion articles for the reproducible proof)
Stick with JIT self-contained when:
Cold start is your only motivation — 70 ms rarely justifies the toolchain cost
You depend on EF Core's productivity and can spend RAM instead of engineering time
Reflection-heavy libraries in your graph fight the trimmer
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.

Join the conversation! Your thoughts help the community grow.