Docker  

Benchmarking .NET 11 Native AOT Container Startup

Container startup time matters when applications are frequently created and destroyed.

This is especially important for workloads such as:

  • Serverless applications

  • Short-lived jobs

  • Kubernetes workloads

  • Autoscaled APIs

  • Background workers

  • Batch processing

  • Build and deployment tasks

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:

CharacteristicJIT-Based .NETNative AOT
Runtime compilationYesNo JIT at runtime
DeploymentRuntime-basedSelf-contained native executable
Startup workRuntime initialization + JIT as neededNative executable initialization
Reflection compatibilityBroadRequires additional analysis
Dynamic codeBroad supportMore restrictions
ArchitectureRuntime-dependentTarget-specific
Container baseRuntime/ASP.NET imagesAOT-compatible runtime-deps images
Optimization targetGeneral runtime executionNative 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:

  • Configuration loading

  • Required dependency initialization

  • Database connectivity

  • Critical cache initialization

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:

MetricPurpose
MinimumFastest observed startup
MedianTypical startup
p95Tail startup behavior
MaximumSlowest 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:

  • Container memory usage

  • Managed memory where applicable

  • RSS

  • CPU during startup

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:

  • Reflection

  • Dynamic code generation

  • Runtime assembly loading

  • Certain serialization patterns

  • Libraries that are not AOT-compatible

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:

VariableJIT BuildNative AOT
Source codeSameSame
Environment variablesSameSame
ConfigurationSameSame
EndpointSameSame
Device/hostSameSame
Container limitsSameSame
DatabaseSameSame
WorkloadSameSame

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:

MetricJIT ContainerNative AOT ContainerDifference
Image sizeMeasureMeasureCalculate
Cold container startupMeasureMeasureCalculate
Median readinessMeasureMeasureCalculate
p95 readinessMeasureMeasureCalculate
First requestMeasureMeasureCalculate
Startup CPUMeasureMeasureCalculate
Memory after readinessMeasureMeasureCalculate
Build timeMeasureMeasureCalculate

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:

  • AOT analyzer warnings

  • Reflection-heavy dependencies

  • Dynamic code generation

  • Runtime-specific libraries

  • Unsupported APIs

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:

  • Native libraries

  • Application allocations

  • Dependency initialization

  • Container base image

Do not attribute the entire memory difference to AOT without evidence.

Best Practices

  1. Define startup precisely before benchmarking.

  2. Compare equivalent JIT and Native AOT builds.

  3. Use Release configuration.

  4. Separate image-pull time from application startup.

  5. Measure readiness rather than process creation alone.

  6. Record median and tail latency.

  7. Use identical CPU and memory limits.

  8. Measure image size separately.

  9. Measure startup CPU and memory.

  10. Benchmark first-request latency separately.

  11. Test the real application, not only a minimal API.

  12. Validate Native AOT compatibility before performance testing.

  13. Test the actual target architecture.

  14. Document the container base image and variant.

  15. Keep external dependencies consistent.

  16. Do not publish fabricated performance numbers.

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