c# / .net

.NET 8, Native AOT, and what a container actually costs

.NET 8, Native AOT, and what a container actually costs

.NET 8 arrived on the fourteenth of November as a long-term support release, and the announcement leads with a number: publishing an ASP.NET Core application as Native AOT cutting startup time by around 80 per cent and application size by around 87 per cent.

Numbers like that are exactly the kind I have argued should not go into a capacity plan without your own measurement. So this is a note about what the feature is, what it costs, and how I would check it — using the same shape of experiment as the .NET 5 container memory post from two years ago.

What Native AOT actually is

Normally a .NET application ships as IL and the runtime JIT-compiles it as it runs. That is why a process is slow for its first few seconds and fast afterwards, and why the container needs a runtime in it.

Native AOT compiles ahead of time to a native executable. No JIT, no IL, and a much smaller runtime linked in. What you deploy is a binary.

.NET 7 had this for console applications. What .NET 8 adds is ASP.NET Core support, focused on minimal APIs, which is the first time it is relevant to the kind of service I actually write.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>

The container that results does not need a .NET base image at all:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
RUN apt-get update && apt-get install -y clang zlib1g-dev
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 -o /out

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 AS final
WORKDIR /app
COPY --from=build /out/Probe ./
USER 1000:1000
ENTRYPOINT ["./Probe"]

runtime-deps carries native dependencies and no runtime. The AOT toolchain needs a native linker at build time, which is why clang appears — a real change to the build image, and worth knowing before a CI pipeline discovers it.

What it costs

The reason this is not a default is that a great deal of ordinary .NET code cannot survive it.

Runtime code generation is gone — anything that emits IL at run time has nothing to emit into. Unbounded reflection is gone, because the trimmer has to decide what to keep by static analysis, and a type only reached through a string name looks unreachable. Most of the serialisation, dependency injection, and ORM machinery people rely on was built on exactly those two capabilities.

Which is why the supported scenario is minimal APIs with source-generated JSON rather than controllers with reflective model binding. Source generators are the compensating mechanism throughout: work that used to happen by reflection at run time moves to code generated at compile time.

The failure mode to be aware of is that trimming warnings are warnings. An application can publish, start, serve most requests, and then throw on the one path that reflects over a type the trimmer removed. Trimming and AOT analysis warnings should be errors in any build that intends to ship this way.

How I would check the numbers

The same discipline as before, because the quantities being quoted are different quantities.

Startup time is the claim most likely to hold, because it is the one with the clearest mechanism: there is no JIT, so there is no warm-up. Measure it as time from container start to first successful response, not as process start, and measure it cold rather than after the image is in page cache.

Size needs care about which size. Published output, uncompressed image, compressed transfer size and the incremental layer size are four different numbers, and an 87 per cent reduction in one of them is not a reduction in the others. The one that matters for deployment is usually the compressed layers you did not already have on the node.

Memory is the one I would be most curious about and least willing to predict. No JIT means no JIT data structures, but the garbage collector is still there and the application's own allocation behaviour is unchanged. Peak under load, sampled the same way on both sides, is the only comparison worth having.

And the comparison has to be the same application on both sides. A minimal API rewritten to be AOT-compatible is not the controller-based application it replaced, so some of any difference is the rewrite rather than the compilation model.

Where I would use it

Startup time is the property with a real business consequence, and it matters most where processes start often: a service that scales out under bursty traffic, a container that is deliberately scaled to zero, a short-lived job.

For a long-running service that starts twice a week, an 80 per cent improvement in startup is 80 per cent of something nobody was waiting for. There, the trade — constrained reflection, a changed build image, a rewrite towards minimal APIs — is being paid for a benefit that does not arrive.

That is the useful way to read any performance release: not "is this faster" but "is the thing it makes faster something I do often". .NET 8 makes a real answer available for the first time in this line of releases. It just is not everybody's question.

Deyan Peev

Written by

Deyan Peev

Founding Engineer · Sofia, Bulgaria

Deyan Peev

Founding Engineer in Sofia, Bulgaria. Currently at 1club.

Elsewhere

© 2026 Deyan Peev