.NET applications have traditionally depended on the .NET runtime and JIT compiler to execute managed code. This model works well for a wide range of applications, but it is not the only deployment option available.
Native AOT (Ahead-of-Time) compilation takes a different approach. Instead of relying on JIT compilation when the application starts, the application is compiled ahead of time into native code for the target platform.
This can change several characteristics of an application, including startup behavior, deployment requirements, memory usage, and compatibility with some .NET features.
Native AOT is particularly interesting for applications such as command-line tools, small services, containerized applications, and workloads where startup time is important.
However, Native AOT is not automatically faster for every application.
The right way to evaluate it is to build the same application using both deployment models and measure the things that matter for your workload.
This article explains how Native AOT works, how to enable it, what to benchmark, and what limitations developers should consider.
What Is Native AOT?
With a traditional .NET deployment, the application contains managed assemblies.
A simplified execution flow looks like this:
C# Source
|
v
IL Assemblies
|
v
.NET Runtime
|
v
JIT
|
v
Native Machine Code
Native AOT changes the model:
C# Source
|
v
IL Assemblies
|
v
Native AOT Compiler
|
v
Native Executable
|
v
Operating System
Much of the compilation work happens before the application is deployed.
The resulting executable is native code for a specific target platform and architecture.
This means Native AOT is not simply a switch that makes a normal .NET application run faster.
It changes the deployment and execution model.
Why Developers Use Native AOT
Native AOT can be useful when an application needs characteristics such as:
Fast startup
Smaller runtime dependency requirements
Self-contained native deployment
Reduced JIT work at startup
Predictable deployment artifacts
Useful behavior for short-lived processes
Consider a command-line utility:
mytool --input data.json
If the tool runs for a few hundred milliseconds, startup overhead can represent a meaningful part of its total execution time.
For a long-running web service that runs continuously for days, startup may matter much less.
This is why workload type is important.
Native AOT vs JIT Deployment
The two approaches have different characteristics.
Area | Traditional JIT | Native AOT |
|---|---|---|
Compilation | At runtime | Before deployment |
Startup work | Includes JIT work | Less JIT work |
Target-specific binary | Runtime generates code for target | Native binary built for target |
Dynamic code | Broad support | More restricted |
Reflection | Broad support | Requires additional consideration |
Deployment | Managed assemblies and runtime | Native executable |
Build process | Generally simpler | More involved |
Best fit | General-purpose applications | Suitable workloads with AOT-compatible dependencies |
Native AOT is therefore a deployment choice, not simply a performance setting.
Enabling Native AOT
For a suitable .NET application, Native AOT can be enabled in the project file.
For example:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>
You can then publish the application for a target runtime.
For example:
dotnet publish -c Release -r linux-x64
The runtime identifier should match the environment where the application will run.
For an Arm64 Linux environment, the target would need to be the appropriate Arm64 runtime identifier instead.
The important point is that Native AOT produces a native binary for a specific target.
A Simple Console Application
Start with a small application:
using System.Diagnostics;
Console.WriteLine("Application started.");
var stopwatch = Stopwatch.StartNew();
long total = 0;
for (int i = 0; i < 10_000_000; i++)
{
total += i;
}
stopwatch.Stop();
Console.WriteLine($"Total: {total}");
Console.WriteLine($"Work time: {stopwatch.ElapsedMilliseconds} ms");
This is useful for experimenting, but it is not enough to conclude that Native AOT is faster.
The test only measures one small piece of work.
A useful evaluation needs to measure startup, execution, memory, and deployment characteristics separately.
Startup Time
Startup is one of the areas where Native AOT can be particularly relevant.
A traditional application may perform several runtime initialization tasks before reaching the main application code.
A Native AOT application has already been compiled to native code.
To test startup, run the application as a separate process and measure the time from process launch to a known output or completion point.
Avoid using:
Stopwatch.StartNew();
inside the application to measure total startup.
That stopwatch starts after the process has already started.
Instead, use an external measurement mechanism or a suitable benchmarking tool.
For example, on Linux, a basic shell-level measurement can be used:
time ./myapp
This provides process-level timing information.
Repeat the test multiple times and compare the same application under both deployment models.
Cold Start vs Warm Start
Startup tests should distinguish between different conditions.
Cold Start
The application is started when relevant code and files are not already benefiting from the operating system's caches.
Warm Start
The application is launched after the operating system has already cached relevant files.
These scenarios can produce different results.
For applications such as serverless functions and short-lived CLI tools, cold-start behavior may be particularly important.
For long-running services, startup may have a much smaller impact on overall performance.
Measuring Memory Usage
Memory usage is another important measurement.
Do not simply compare executable file sizes and assume that the smaller file consumes less memory at runtime.
Measure the running process.
A practical test should record:
Initial memory
Steady-state memory
Peak memory
Allocation behavior
Garbage collection activity
For a long-running service, steady-state behavior may matter more than startup memory.
For a short-lived process, startup and peak memory can be more relevant.
Throughput Testing
Native AOT should also be tested against the actual workload.
For example, imagine a service processing messages:
public static int Process(int value)
{
return value * 2;
}
A benchmark might process many values:
public static long ProcessValues(int[] values)
{
long total = 0;
for (int i = 0; i < values.Length; i++)
{
total += Process(values[i]);
}
return total;
}
Now compare the same application deployed using normal JIT execution and Native AOT.
The important question is not whether one test is faster.
The question is whether the difference remains meaningful under the application's real workload.
Native AOT Does Not Mean No Runtime
Native AOT applications are native executables, but they still rely on runtime support and libraries.
The important distinction is that application code is compiled ahead of time instead of being JIT-compiled in the normal way at runtime.
Native AOT also has a different set of supported features and restrictions.
This is particularly important for applications that depend heavily on runtime code generation or unrestricted reflection.
Reflection Requires Attention
Reflection is widely used in .NET applications.
For example:
Type type = typeof(Customer);
PropertyInfo[] properties =
type.GetProperties();
Applications that dynamically discover types, members, or assemblies may require additional work when using Native AOT.
Native AOT relies heavily on knowing what code is required at build time.
If the compiler cannot determine that a type or member is required, the application may need appropriate metadata or code annotations.
This is one of the biggest areas to investigate before migrating an existing application.
Dynamic Code Can Be a Problem
Some libraries generate code dynamically at runtime.
Examples can include:
Runtime proxies
Dynamic serializers
Expression compilation
Runtime assembly generation
Certain reflection-heavy frameworks
An application may work correctly under the normal JIT deployment model and then require changes for Native AOT.
This is why compatibility testing should happen before performance testing.
There is no value in benchmarking an AOT build that cannot correctly execute the application's actual features.
A Practical Testing Process
A reliable Native AOT evaluation can follow these steps.
Step 1: Choose a Representative Application
Do not start with a toy application unless you are learning how the technology works.
For a real migration decision, use a representative service or tool.
Step 2: Build the Normal Version
Publish the application using the normal deployment model.
For example:
dotnet publish -c Release
Step 3: Enable Native AOT
Add the appropriate project configuration:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
Then publish for the target platform:
dotnet publish -c Release -r linux-x64
Step 4: Verify Functionality
Before measuring performance, verify:
Application startup
API behavior
Serialization
Configuration
Logging
Database access
Authentication
Error handling
External integrations
Step 5: Measure Startup
Run the same executable repeatedly and record startup behavior.
Step 6: Measure Memory
Measure both startup and steady-state memory.
Step 7: Measure Throughput
Run realistic workloads against both versions.
Step 8: Compare Results
Use the same hardware, operating system, configuration, and input data.
Benchmarking Application Code
For CPU-focused comparisons, BenchmarkDotNet can be useful.
For example:
using BenchmarkDotNet.Attributes;
public class ProcessingBenchmark
{
private readonly int[] values = new int[100_000];
[Benchmark]
public long Process()
{
long total = 0;
for (int i = 0; i < values.Length; i++)
{
total += values[i] * 2;
}
return total;
}
}
This measures application code rather than complete process startup.
That distinction matters.
Use process-level measurements for startup and application-level benchmarks for CPU operations.
Do not use one benchmark to answer every performance question.
What Should You Measure?
A useful comparison might look like this:
Metric | Why it matters |
|---|---|
Startup time | Important for short-lived processes |
First request latency | Useful for services |
Steady-state latency | Shows normal runtime behavior |
Throughput | Shows processing capacity |
Peak memory | Important for constrained environments |
Steady-state memory | Useful for long-running services |
CPU usage | Helps identify processing differences |
Binary size | Important for deployment and distribution |
Build time | AOT can increase build complexity |
Compatibility | Required before performance comparisons |
This gives a much more complete picture than simply comparing execution time.
Common Mistakes
Comparing Debug Builds
Always use the appropriate Release configuration when performing performance testing.
dotnet publish -c Release
Testing Different Hardware
If the JIT version runs on one server and the AOT version runs on another, the comparison is difficult to interpret.
Use the same environment where possible.
Measuring Only Startup
Native AOT can change startup characteristics, but that does not tell you how the application behaves after it has been running for hours.
Measuring Only CPU Time
A service may have excellent CPU performance but still have a memory or I/O bottleneck.
Ignoring Compatibility
An application that cannot use an important dependency under AOT is not a successful AOT candidate regardless of benchmark results.
Best Practices
Keep a Baseline
Before changing the deployment model, record the existing application's performance.
Use Production-Like Workloads
If production processes JSON requests, test JSON requests.
If production processes files, test representative files.
Test on the Target Architecture
An AOT binary is built for a specific target environment.
Test it where it will actually run.
Separate Startup and Steady-State Tests
Do not mix the two measurements.
Test Multiple Runs
One run can be affected by system activity and caching.
Use repeated measurements and look for consistent results.
Check Dependencies Early
Review the libraries used by the application before committing to an AOT migration.
Monitor Real Applications
If the application is deployed, continue measuring real behavior after the change.
Advantages of Native AOT
Native AOT can provide several practical benefits for suitable applications:
Less JIT work at startup
Native executable deployment
Potentially useful startup characteristics
Reduced dependency on runtime compilation
Useful fit for certain short-lived workloads
Can simplify deployment for some environments
The actual benefit depends on the application.
Disadvantages and Trade-Offs
Native AOT also introduces trade-offs:
Some .NET features require additional consideration
Reflection-heavy applications may need changes
Some libraries may not be AOT-compatible
Builds can become more involved
Native binaries are target-specific
Debugging and diagnostics can differ from a normal managed deployment
Dynamic code generation has restrictions
These are not reasons to avoid Native AOT.
They are reasons to evaluate it against the actual application.
When Native AOT Is Worth Testing
Native AOT is particularly worth evaluating for:
Command-line tools
Short-lived workers
Small services
Container workloads
Applications where startup matters
Resource-constrained environments
It may be less compelling when:
The application runs continuously for long periods.
Startup time is insignificant.
The application relies heavily on dynamic runtime behavior.
Dependencies are not compatible with AOT.
The main bottleneck is database or network I/O.
Again, these are starting points rather than universal rules.
Troubleshooting Native AOT Builds
If publishing fails, start by examining the build output.
Common areas to investigate include:
Reflection
Look for code that discovers types or members dynamically.
Dynamic Code Generation
Check whether a dependency requires runtime-generated code.
Serialization
Verify that the serializer and configuration are compatible with the AOT deployment model.
Dependencies
A library used by the application may have AOT limitations.
Platform Target
Make sure the runtime identifier matches the actual deployment environment.
For example:
dotnet publish -c Release -r linux-x64
should not be used to produce the binary intended for a different architecture.
A Simple Evaluation Matrix
Before adopting Native AOT, document the results.
Test | JIT Deployment | Native AOT | Observation |
|---|---|---|---|
Startup | Measure | Measure | Compare repeated runs |
First request | Measure | Measure | Use same request |
Throughput | Measure | Measure | Same workload |
Peak memory | Measure | Measure | Same environment |
Steady-state memory | Measure | Measure | Long-running test |
CPU usage | Measure | Measure | Same workload |
Build time | Measure | Measure | Include CI build |
Compatibility | Verify | Verify | All required features |
The purpose of this table is not to produce a single winner.
It gives the team enough information to decide whether the deployment model fits the application's requirements.
Summary
Native AOT changes how a .NET application is compiled and deployed. Instead of depending on normal JIT compilation for application code at runtime, the application is compiled ahead of time into a native executable for a specific target environment.
The most useful way to evaluate Native AOT is through measurement. Test startup time, first-request behavior, steady-state performance, memory usage, CPU usage, binary size, build time, and application compatibility.
Native AOT can be a good fit for applications where startup and deployment characteristics matter, but it is not a universal performance switch. Applications that rely heavily on reflection, dynamic code generation, or incompatible dependencies may require additional changes.
For developers considering Native AOT, the safest approach is to establish a baseline, build an AOT version, verify that all required functionality still works, and then compare both versions using the same hardware and realistic workloads. That gives you evidence about whether Native AOT is useful for your application instead of relying on a generic performance claim.

Join the conversation! Your thoughts help the community grow.