Container startup time matters when applications are frequently created and destroyed.
This is especially important for workloads such as:
For a long-running application, startup may happen only once every few days or weeks.
For a highly dynamic container platform, the same startup cost can occur thousands of times.
Native AOT changes the application execution model by compiling .NET code to native code ahead of time. Native AOT applications do not require JIT compilation at runtime and are self-contained. Microsoft documents faster startup and smaller memory footprints as key benefits of the deployment model, particularly for workloads with many deployed instances.
.NET 11 continues to improve Native AOT, including runtime work such as faster interface dispatch. Preview 6 also includes smaller Native AOT SDK container images.
That makes startup benchmarking an important way to determine whether Native AOT is actually beneficial for a particular containerized application.
What Is Native AOT?
A normal .NET application can use JIT compilation at runtime.
A simplified model is:
.NET Application
|
v
CLR
|
v
JIT Compilation
|
v
Native Code
|
v
Application Execution
Native AOT changes this:
.NET Application
|
v
AOT Compiler
|
v
Native Binary
|
v
Container
|
v
Application Execution
The native binary does not need to perform JIT compilation when the container starts.
This can reduce work during the startup path, although the actual startup time still depends on the application, container image, operating system, initialization logic, and deployment environment.
Why Container Startup Matters
Consider a Kubernetes deployment:
Load
|
v
Kubernetes
|
+----------+----------+
| | |
v v v
Pod #1 Pod #2 Pod #3
|
v
Application
If traffic increases, additional pods may be created.
The overall response time can therefore include:
Scheduling
+
Image Availability
+
Container Creation
+
Application Startup
+
Readiness
Native AOT primarily affects the application portion of this sequence.
It does not automatically eliminate Kubernetes scheduling, image pulling, networking, or infrastructure delays.
This distinction is important when designing a benchmark.
Native AOT vs JIT-Based Containers
A useful comparison is:
| Characteristic | JIT-Based .NET | Native AOT |
|---|
| Runtime compilation | Yes | No JIT at runtime |
| Deployment | Runtime-based | Self-contained native executable |
| Startup work | Runtime initialization + JIT as needed | Native executable initialization |
| Reflection compatibility | Broad | Requires additional analysis |
| Dynamic code | Broad support | More restrictions |
| Architecture | Runtime-dependent | Target-specific |
| Container base | Runtime/ASP.NET images | AOT-compatible runtime-deps images |
| Optimization target | General runtime execution | Native published workload |
Native AOT is not simply a switch that makes every application better.
The deployment model has compatibility constraints that must be tested.
Define the Benchmark Objective
A useful benchmark should answer a specific question.
For example:
How does container readiness time change when
the same ASP.NET Core application is published
using the standard runtime model versus Native AOT?
This is better than:
Is Native AOT faster?
The first question can be measured.
The second is too broad.
Establish Two Builds
Create two comparable application builds.
Standard .NET Build
ASP.NET Core
|
v
JIT-Based Runtime
|
v
Container
Native AOT Build
ASP.NET Core
|
v
Native AOT
|
v
Native Binary
|
v
Container
Keep the application source, configuration, endpoint behavior, and test workload the same.
The deployment model should be the primary experimental variable.
Enable Native AOT
For a compatible project, Native AOT can be enabled through the project file:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
Microsoft recommends configuring PublishAot in the project file because it controls more than the publish command itself, including analysis related to dynamic code usage.
Then publish for the target runtime:
dotnet publish \
-c Release \
-r linux-x64
The runtime identifier must match the deployment environment.
For example:
linux-x64
linux-arm64
are different native targets.
A binary compiled for one target architecture cannot simply be treated as a universal binary.
Build the Container
A simplified multi-stage Dockerfile can look like:
FROM mcr.microsoft.com/dotnet/sdk:11.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish \
-c Release \
-r linux-x64 \
-o /app/publish
FROM mcr.microsoft.com/dotnet/runtime-deps:11.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["./MyApi"]
The exact image tag should match the .NET version and supported image variant being tested.
Microsoft provides dedicated runtime-deps image variants for Native AOT scenarios. Its container-image documentation also identifies *-aot variants for supported distributions.
Why runtime-deps Is Used
A Native AOT application is already compiled into a native executable.
It does not require the normal .NET runtime image in the same way a JIT-based application does.
The runtime-deps images are designed for scenarios including self-contained and Native AOT applications. Microsoft also provides AOT-specific variants for supported distributions.
Conceptually:
JIT Application
|
v
ASP.NET Runtime Image
Native AOT
|
v
Native Binary
|
v
runtime-deps Image
The exact base image should be selected based on operating-system, globalization, architecture, and application requirements.
Do Not Benchmark the Build Stage
A common mistake is to measure:
Docker build
+
Image startup
as if both represent runtime performance.
They are separate concerns.
Native AOT can have a different build-time cost because native compilation occurs during publishing.
Measure:
Build Time
separately from:
Container Startup
This produces a much more useful engineering comparison.
Define Container Startup
"Startup" can mean several different things.
Possible definitions include:
T0
Container process starts
T1
Application process starts
T2
HTTP server starts listening
T3
Readiness endpoint succeeds
T4
First successful application request
For production scaling, readiness time is often more useful than simply measuring process creation.
For example:
Container Start
|
v
Application Start
|
v
Kestrel Listening
|
v
Initialization
|
v
/readiness = 200
Define the measurement point before comparing builds.
Add a Readiness Endpoint
For example:
app.MapGet(
"/readiness",
() => Results.Ok());
Then the benchmark can wait for:
GET /readiness
to return successfully.
This creates a consistent application-level startup signal.
In a real production application, the readiness condition may also include:
Do not add external dependencies to the benchmark unless they are part of the startup behavior you want to measure.
Measure With Docker
A simple manual test can start a container:
docker run \
--rm \
-p 8080:8080 \
myapi:aot
Then measure the time until:
GET /readiness
returns successfully.
For repeatable research, automate the process.
The benchmark should:
1. Start container
2. Record start timestamp
3. Poll readiness
4. Record ready timestamp
5. Stop container
6. Repeat
Repeat the Test
Never rely on one startup measurement.
Use multiple runs:
Run 1
Run 2
Run 3
Run 4
Run 5
...
Then report a distribution.
Useful metrics include:
| Metric | Purpose |
|---|
| Minimum | Fastest observed startup |
| Median | Typical startup |
| p95 | Tail startup behavior |
| Maximum | Slowest observed startup |
The number of runs should be sufficient for the workload and environment.
The important point is repeatability.
Separate Cold and Warm Conditions
Container startup can be affected by image availability.
Consider:
Cold Image
Image not locally available
|
v
Pull
|
v
Container Start
versus:
Warm Image
Image already available
|
v
Container Start
If you include image pulling, you are measuring:
Image Distribution
+
Container Startup
+
Application Startup
If you want application startup specifically, use an already available image.
Both measurements can be useful, but they answer different questions.
Benchmark Image Pull Separately
For deployment analysis, measure:
Image Size
+
Pull Time
+
Container Startup
+
Readiness
For runtime analysis, measure:
Already Available Image
+
Container Startup
+
Readiness
This prevents image-transfer time from hiding the effect of Native AOT.
Measure Image Size
Native AOT can be attractive for container workloads because it can produce compact runtime artifacts.
However, do not assume that every Native AOT container will be smaller than every JIT-based container.
Measure the actual images.
For example:
docker image inspect myapi:aot
docker image inspect myapi:jitted
Then compare:
Compressed Registry Size
Uncompressed Local Size
Layer Count
Largest Layers
Registry size is particularly relevant for deployment and autoscaling.
Measure Memory After Startup
Startup time is only one Native AOT characteristic.
Also measure memory:
Container Starts
|
v
Application Ready
|
v
Memory Measurement
Useful measurements include:
Microsoft documents smaller memory footprints as one of the benefits of Native AOT, but the actual result remains workload-dependent.
Measure CPU During Startup
Startup CPU can reveal why one build starts faster.
For example:
Startup
|
+-- CPU spike
+-- Runtime initialization
+-- JIT work
+-- Application initialization
A Native AOT application may have a different startup CPU profile.
Do not infer the reason solely from the final startup number.
Use runtime and container telemetry where available.
Test a Minimal API First
A minimal application can establish a controlled baseline.
For example:
var builder =
WebApplication.CreateBuilder(args);
var app =
builder.Build();
app.MapGet(
"/",
() => "Hello World");
app.MapGet(
"/readiness",
() => Results.Ok());
app.Run();
Benchmark this first.
Then benchmark the real application.
This separates:
Runtime/container overhead
from:
Application initialization
Test the Real Application
A minimal API benchmark is useful but insufficient for production decisions.
A real application may initialize:
Dependency Injection
Configuration
Logging
Database
Telemetry
Caches
Authentication
HTTP Clients
Message Queues
Native Libraries
A real startup sequence may look like:
Container
|
v
.NET Startup
|
+-- Configuration
+-- DI
+-- Telemetry
+-- Database
+-- Cache
+-- Application Services
|
v
Readiness
Measure this workflow separately.
Native AOT Compatibility Matters
Native AOT has restrictions that do not apply to ordinary JIT execution.
Applications using dynamic behavior may require changes.
Examples include:
Microsoft provides AOT analyzers and recommends marking libraries appropriately with IsAotCompatible when applicable.
For example:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
This is not a magic compatibility switch.
It communicates compatibility and enables related analysis.
Test Reflection-Heavy Applications
Consider code such as:
var type =
Type.GetType(typeName);
Native AOT may require additional information for dynamically accessed types.
The application should be tested with the actual publish configuration.
Do not conclude that:
Application runs in Debug
means:
Native AOT publish will work correctly
The publish process can expose compatibility problems.
Test Serialization
Serialization libraries may use reflection or source-generated approaches.
For applications using System.Text.Json, consider source generation where appropriate.
A simplified pattern is:
[JsonSerializable(typeof(Customer))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
Then:
var json =
JsonSerializer.Serialize(
customer,
AppJsonContext.Default.Customer);
The exact implementation depends on the application's serialization model.
The important point is to identify dynamic serialization paths before enabling Native AOT.
Benchmark the Same Application Configuration
Keep these variables constant:
| Variable | JIT Build | Native AOT |
|---|
| Source code | Same | Same |
| Environment variables | Same | Same |
| Configuration | Same | Same |
| Endpoint | Same | Same |
| Device/host | Same | Same |
| Container limits | Same | Same |
| Database | Same | Same |
| Workload | Same | Same |
Only then can you reasonably attribute differences to the deployment model.
Control Container Resources
Run both builds with the same limits.
For example:
docker run \
--cpus="1.0" \
--memory="512m" \
myapi:aot
Then use the same limits for the JIT-based image.
Otherwise, the benchmark is not comparable.
You can repeat the experiment with different resource limits:
0.5 CPU
1 CPU
2 CPU
4 CPU
This can reveal whether the startup difference changes under constrained resources.
Test Multiple Architectures
Native AOT binaries are target-specific.
For example:
linux-x64
linux-arm64
should be benchmarked separately when both architectures are supported by the deployment platform.
Do not compare an ARM machine running one build with an x64 machine running another and attribute every difference to Native AOT.
Hardware architecture is an independent variable.
Test Container Base Images
The base image can influence:
Image size
Startup environment
Available libraries
Globalization behavior
Native dependencies
Microsoft provides several image families, including Alpine, Ubuntu chiseled, and other variants, with different runtime and globalization characteristics.
For a serious benchmark, document:
Base image
Distribution
Architecture
Image variant
Globalization configuration
Globalization Can Change the Image Choice
Some minimal container images omit globalization dependencies such as ICU and tzdata.
Microsoft's container-image documentation notes that chiseled and similar minimal images can require globalization-invariant configuration, while Ubuntu and Debian images include relevant globalization dependencies.
For example:
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
Do not enable invariant globalization simply to make an image smaller.
First verify that the application does not require full globalization behavior.
Benchmark Container Restart
A useful experiment is repeated container creation:
Start
Ready
Stop
Start
Ready
Stop
Start
Ready
Stop
This models workloads where containers are frequently replaced.
It can also reveal whether startup behavior changes across repeated runs.
Benchmark Scale-Out
Native AOT is particularly interesting for high-instance workloads.
For example:
1 instance
|
v
10 instances
|
v
100 instances
Calculate total startup work:
Total Startup Cost =
Instances × Startup Cost
This does not mean that Native AOT automatically reduces infrastructure cost by the same percentage as startup time.
Image distribution, orchestration, networking, and application initialization all contribute to deployment behavior.
Compare Readiness Under Load
A container can start quickly but become ready slowly because initialization depends on external services.
Test:
Application
|
+-- Database
+-- Cache
+-- External API
Then compare whether the same dependencies dominate both builds.
If both builds spend most startup time waiting for a database, changing the runtime may have little effect on readiness.
Benchmark First Request Separately
Measure:
Container Start
|
v
Ready
|
v
First Request
The time from readiness to the first request can differ from startup time.
A useful measurement set is:
Process Start
Readiness
First Request
Steady-State Request
This helps identify whether a Native AOT change affects actual user-visible latency.
Example Benchmark Table
Use a table such as:
| Metric | JIT Container | Native AOT Container | Difference |
|---|
| Image size | Measure | Measure | Calculate |
| Cold container startup | Measure | Measure | Calculate |
| Median readiness | Measure | Measure | Calculate |
| p95 readiness | Measure | Measure | Calculate |
| First request | Measure | Measure | Calculate |
| Startup CPU | Measure | Measure | Calculate |
| Memory after readiness | Measure | Measure | Calculate |
| Build time | Measure | Measure | Calculate |
The values should come from the actual test environment.
Do not publish hypothetical numbers as benchmark results.
Common Benchmarking Mistakes
Measuring Docker Image Pull Time as Application Startup
Separate image distribution from process startup.
Using Different Resource Limits
CPU and memory limits must remain comparable.
Benchmarking Debug Builds
Use Release builds for production-oriented performance testing.
Testing Only a Hello World Application
A minimal API is useful for baseline measurements but cannot represent a complex production application.
Ignoring Native AOT Compatibility
A faster binary is irrelevant if important application features cannot run correctly.
Comparing Different Hardware
Use the same machine and architecture for the primary comparison.
Measuring Only One Run
Startup measurements are affected by system conditions.
Ignoring First Request Latency
Readiness and first-request behavior can tell different stories.
Reporting Only the Best Result
Report distributions such as median and p95.
Troubleshooting
Native AOT Publish Fails
Check:
Do not suppress warnings without understanding their impact.
Container Starts but Application Fails
Check the Native AOT binary's target architecture and required native libraries.
Then verify the selected runtime-deps image.
Image Is Larger Than Expected
Inspect the layers.
Check:
Application
Native dependencies
Runtime dependencies
Resources
Debug symbols
AOT does not guarantee the smallest possible container for every application.
Startup Is Not Faster
Measure the startup stages individually.
For example:
Runtime initialization
|
v
Application initialization
|
v
Database connection
|
v
Readiness
If application initialization dominates, runtime changes may have limited effect.
Memory Usage Is Higher
Compare the same workload and resource conditions.
Then determine whether the difference comes from:
Do not attribute the entire memory difference to AOT without evidence.
Best Practices
Define startup precisely before benchmarking.
Compare equivalent JIT and Native AOT builds.
Use Release configuration.
Separate image-pull time from application startup.
Measure readiness rather than process creation alone.
Record median and tail latency.
Use identical CPU and memory limits.
Measure image size separately.
Measure startup CPU and memory.
Benchmark first-request latency separately.
Test the real application, not only a minimal API.
Validate Native AOT compatibility before performance testing.
Test the actual target architecture.
Document the container base image and variant.
Keep external dependencies consistent.
Do not publish fabricated performance numbers.
Repeat the benchmark after major .NET or application changes.
Frequently Asked Questions
Does Native AOT always start faster than a JIT-based .NET container?
No universal startup improvement should be assumed.
Native AOT eliminates runtime JIT compilation and is designed for fast startup, but actual application readiness depends on initialization work, dependencies, container configuration, and infrastructure. Microsoft documents faster startup as a Native AOT benefit while emphasizing workload characteristics.
Does Native AOT require a .NET runtime container?
A Native AOT application is self-contained and compiled to native code. Microsoft provides runtime-deps images and AOT-specific variants for Native AOT scenarios rather than requiring the standard ASP.NET Core runtime image.
Does Native AOT make Docker images smaller?
It can, but image size should be measured rather than assumed.
The final size depends on the application, native dependencies, base image, architecture, resources, and image variant.
Does Native AOT eliminate JIT completely?
Native AOT applications do not use a JIT compiler at runtime. The application is compiled ahead of time to native code.
Is Native AOT suitable for every ASP.NET Core application?
No.
Applications that depend heavily on dynamic code generation, reflection patterns, or libraries without appropriate AOT compatibility may require changes.
Microsoft provides AOT compatibility analyzers to help identify issues.
Should I benchmark Native AOT on both x64 and ARM64?
If both architectures are production targets, yes.
Native AOT produces architecture-specific native binaries, so each target should be evaluated independently.
Should build time be included in the startup benchmark?
No.
Build/publish time should be measured separately. Runtime startup should measure the deployed container from process creation through the chosen readiness condition.
Conclusion
Native AOT changes the way a .NET application is packaged and executed.
Instead of relying on runtime JIT compilation:
.NET Application
|
v
JIT Runtime
|
v
Native Code
the application is compiled ahead of time:
.NET Application
|
v
Native AOT Compiler
|
v
Native Binary
|
v
Container
That makes Native AOT particularly interesting for container workloads where startup time, memory usage, and instance density matter.
.NET 11 continues to improve the Native AOT experience, including runtime improvements and smaller Native AOT SDK container images in Preview 6.
However, the correct engineering approach is measurement rather than assumption.
A useful benchmark should compare:
Image Size
+
Container Startup
+
Readiness
+
First Request
+
Startup CPU
+
Memory
+
Build Time
while keeping the environment controlled.
The most important principle is:
Benchmark the complete deployment path, but keep image distribution, infrastructure scheduling, application initialization, and runtime startup as separate measurements.
Native AOT may provide substantial value for high-instance, short-lived workloads, but the decision should ultimately come from measurements of the application's real container, real dependencies, real architecture, and real deployment environment.