📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is ReadyToRun (R2R), and when should you publish with it?

ReadyToRun precompilation with crossgen2: startup benefits, size cost, version resilience, composite images and how it interacts with tiering.

Mid-Level Theory Advanced clr crossgen publish readytorun startup

🎯 Key points

  • ReadyToRun adds precompiled native code to assemblies while keeping IL
  • Enable with PublishReadyToRun and a runtime identifier; crossgen2 does the compilation
  • It improves startup; hot methods are rejitted at Tier 1 for steady-state speed
  • R2R code is version-resilient across version bubbles and so slightly less optimised
  • Assemblies get larger; composite R2R can compile app and framework together
  • Unlike NativeAOT, the JIT and full reflection remain available

⏱ 30-second answer

ReadyToRun is ahead-of-time compilation that keeps the IL and adds precompiled native code to each assembly. Publish with <PublishReadyToRun>true</PublishReadyToRun> and a runtime identifier. At startup the runtime uses the native code instead of JIT-compiling, which cuts startup time; hot methods are still rejitted at Tier 1 so steady-state performance is kept. Costs: assemblies are larger (often two to three times) and R2R code is a little less optimised because it must stay valid across versions of dependencies outside its version bubble. Use it for services where cold start matters; it is still a JIT-based app, unlike NativeAOT.

🗣 Full answer (senior)

How I would answer this in an interview

ReadyToRun (R2R) is a publishing option that precompiles IL to native code ahead of time (using the crossgen2 compiler) and stores that code alongside the IL in the same assemblies. At start-up, the runtime uses the precompiled code instead of JIT-compiling those methods, so applications start faster. Hot methods are still re-JIT-ed later at Tier 1 for peak performance. It's a middle ground between pure JIT and NativeAOT.

1. How to publish with it

<PropertyGroup>
  <PublishReadyToRun>true</PublishReadyToRun>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>   <!-- R2R code is platform-specific -->
</PropertyGroup>
dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true

Options: PublishReadyToRunComposite (compile app + framework into a composite image for further start-up gains, typically with self-contained), PublishReadyToRunEmitSymbols, partial R2R via PublishReadyToRunExclude.

The framework (shared runtime libraries) is already shipped as R2R; this option compiles your code and dependencies.

2. What you get

  • Faster start-up: fewer methods JIT-compiled at launch — often 30–60% less start-up time for large apps (ASP.NET Core with many dependencies, desktop apps).
  • Faster first request (less JIT on cold paths).
  • Less CPU at start-up — valuable for scale-out events in containers and serverless.
  • Full compatibility: it's still a normal JIT-capable app — reflection, dynamic code, plugins all work.
  • Steady state unchanged (or close): hot methods are re-JIT-ed at Tier 1 with dynamic PGO.

3. Costs and limitations

  • Larger files: assemblies contain both IL and native code (often 2–3× bigger).
  • Platform-specific: you must publish per RID (linux-x64, linux-arm64, win-x64).
  • R2R code is less optimised than Tier 1: it must be version-resilient (can't inline across version bubble boundaries freely, uses indirections for cross-assembly calls) and has no dynamic PGO. That's why tiering re-JITs hot methods.
  • Not everything is precompiled: some generic instantiations across assemblies, methods with certain features, or code requiring runtime-specific layout fall back to JIT.
  • Longer build/publish times.
  • Version bubble rules: by default each assembly is its own bubble; composite/large bubbles allow more cross-assembly inlining but require the exact versions at run time.

4. R2R vs JIT-only vs NativeAOT

JIT only ReadyToRun NativeAOT
Start-up Slowest Faster Fastest
Steady-state Best (Tier 1 + PGO) Same (re-JIT hot methods) Good, no dynamic PGO
Size Smallest Larger Self-contained native binary (often smaller than self-contained JIT apps when trimmed)
Compatibility Full Full Restricted (no JIT, trimming, reflection limits)
Platform-specific No Yes Yes

5. When to publish with R2R

Good fit:

  • Services that scale out frequently (Kubernetes/ACA autoscaling, Azure Functions) where cold start affects latency or cost.
  • Large apps with heavy start-up (many DI registrations, EF Core models, MVC).
  • Desktop/CLI tools where launch time matters.
  • When NativeAOT isn't feasible due to reflection-heavy dependencies.

Less valuable:

  • Long-running services that rarely restart and aren't latency-sensitive at start-up.
  • When image size is a hard constraint.
  • When you can use NativeAOT successfully (bigger start-up win).

Measure: compare start-up time to first successful request, and JIT counts (dotnet-counters methods-jitted-count) with and without R2R.

6. A production example

An ASP.NET Core API on Azure Container Apps scaled from 2 to 20 replicas during morning peaks; new replicas took ~4 s to become ready and served slow first requests. Publishing with PublishReadyToRun=true (linux-x64) cut ready time to ~1.8 s and reduced first-request latency by about half; image size grew by ~40 MB, which was acceptable. NativeAOT wasn't an option because of EF Core features and a reflection-heavy reporting library, so R2R gave most of the benefit without compatibility risk.

🔍 What the interviewer is checking

The interviewer checks you know the startup options and their trade-offs.

Green flags: IL plus native code, needs RID, rejit at Tier 1, version bubble, size cost, comparison with NativeAOT, measured result.

Red flags: "R2R means no JIT"; claims it makes everything faster; cannot say when not to use it.

📚 Learn in depth

1. How R2R code is used at run time

When a method is first called, the runtime checks whether the R2R image contains precompiled code for it and whether its fixups (references to types, methods, fields in other assemblies) can be resolved and are valid for the loaded versions. If so, it uses that code (no JIT). Otherwise it JITs. With tiering, R2R code counts as "tier 0-ish": hot methods get call counting and are recompiled at Tier 1.

2. Version resilience

R2R code must remain valid if dependencies are serviced (patched) independently. So crossgen2 avoids baking in assumptions like field offsets of types from other version bubbles, or inlining methods from other assemblies, using indirection cells resolved at run time. This makes R2R code somewhat slower than JIT Tier 1 code.

3. Composite R2R

Composite images compile multiple assemblies (optionally including the framework) into a single native image with a shared version bubble, enabling cross-assembly inlining and reducing fixups — better start-up and code quality, but requires deploying exactly those versions together (self-contained).

4. Partial R2R and profiles

You can use profile data (MIBC files from training runs, via dotnet-pgo) to compile only hot start-up methods, reducing size while keeping start-up gains ("partial R2R"). The .NET framework itself is compiled with static PGO profiles.

5. Diagnosing

  • DOTNET_ReadyToRun=0 disables use of R2R code to compare.
  • DOTNET_ReadyToRun_DisableR2R... style switches and ETW/EventPipe events (R2R get-entry-point events) show usage.
  • dotnet-counters: lower JIT counts at start-up with R2R.
  • R2RDump tool inspects images.

Try it yourself

  1. Publish an API with and without PublishReadyToRun and measure time to first response.
  2. Compare assembly sizes.
  3. Use DOTNET_JitDisasmSummary=1 to see which methods are still JIT-ed with R2R.
  4. Try composite R2R with a self-contained publish.

💻 Code

<!-- MyService.csproj -->
<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
using System.Diagnostics;

var sw = Stopwatch.StartNew();

// Representative startup work: configuration, JSON, LINQ
var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = false };
string json = System.Text.Json.JsonSerializer.Serialize(
    Enumerable.Range(1, 100).Select(i => new { Id = i, Name = $"Item {i}" }).ToList(), options);

Console.WriteLine($"Startup work done in {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"Methods JIT-compiled: {System.Runtime.JitInfo.GetCompiledMethodCount()}");
Console.WriteLine($"JIT time: {System.Runtime.JitInfo.GetCompilationTime().TotalMilliseconds:N0} ms");
dotnet publish -c Release -o out-r2r
DOTNET_ReadyToRun=0 ./out-r2r/MyService   # same binary, R2R code ignored, for comparison
./out-r2r/MyService

⚠ Common mistakes

1. Expecting faster steady state

What happens: enabling R2R to improve throughput. Why it is wrong: R2R mainly helps start-up; hot code is re-JIT-ed. Do instead: use it for cold start; profile steady state separately.

2. Publishing without a RuntimeIdentifier

What happens: expecting portable R2R output. Why it is wrong: R2R code is platform-specific. Do instead: publish per target RID.

3. Ignoring image size

What happens: container images grow significantly. Why it is wrong: slower pulls and more storage. Do instead: measure trade-offs; consider partial R2R.

4. Disabling tiering with R2R

What happens: turning off tiered compilation. Why it is wrong: R2R code stays less optimised forever (or everything is fully JIT-ed at start-up). Do instead: keep tiering on.

5. Assuming all code is precompiled

What happens: expecting zero JIT at start-up. Why it is wrong: some methods and generics still JIT. Do instead: measure JIT counts.

6. Choosing R2R when NativeAOT fits

What happens: using R2R for small, AOT-compatible services needing minimal cold start. Why it is wrong: NativeAOT gives bigger start-up and memory wins. Do instead: evaluate AOT compatibility first.

7. Not measuring

What happens: enabling R2R without baseline measurements. Why it is wrong: can't justify size/build costs. Do instead: measure time-to-ready and first-request latency.

8. Mixing versions with composite images

What happens: servicing individual assemblies in a composite deployment. Why it is wrong: composite code assumes exact versions. Do instead: redeploy the whole composite set.

🏛 Senior insight: trade-offs

ReadyToRun is the low-risk startup lever: no code changes, full compatibility.

Need Choice
Faster cold start, keep all features ReadyToRun
Even faster start, maximum framework inlining, self-contained Composite R2R
Smallest, fastest start and you can accept restrictions NativeAOT
Long-running service, no scaling pressure Plain JIT, smaller images

Seniors measure: time to first response, image size and steady-state throughput, and pick based on numbers rather than habit.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate compares R2R with full AOT Why: Checks NativeAOT trade-offs and restrictions
    How to answer

    NativeAOT (<PublishAot>true</PublishAot>) compiles the whole app, with the parts of the runtime it needs, into one native executable with no JIT and no IL at run time. You get very fast startup, lower memory and a smaller deployment with no .NET runtime to install. The cost is a closed world: trimming is mandatory, Reflection.Emit and loading new assemblies at run time are not available, reflection works only on what the compiler can see, and libraries must be AOT-compatible. ASP.NET Core supports it for minimal APIs and gRPC but not MVC or Razor Pages. Fix the IL2xxx and IL3xxx warnings before shipping.

  2. When: Candidate says hot R2R methods are rejitted Why: Checks tiered compilation knowledge
    How to answer

    Tiered compilation (on by default since .NET Core 3.0) compiles a method first at Tier 0: fast to produce, barely optimised. The runtime counts calls; a method called about 30 times (after a short startup delay) is recompiled at Tier 1 with full optimisation on a background thread and the entry point is swapped. ReadyToRun code counts as a starting tier and is also promoted when hot. On-stack replacement (default on x64 and Arm64 since .NET 7) lets a long-running loop in a Tier 0 method jump into optimised code mid-execution. The result is fast startup and good steady-state performance, but benchmarks must warm up.

  3. When: Candidate mentions runtime identifiers and publish Why: Probes publish options and deployment models
    How to answer

    By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.

🛠 Real-world problems: how you handle them

Slow pod readiness during traffic bursts fixed with ReadyToRun
Situation
A checkout service on Kubernetes scaled from [4 to 40] pods during sales events.
Signals
New pods took [several seconds] to serve the first request; the HPA added pods but latency stayed high during scale-out.
Investigation
Measured JIT time with `JitInfo` and `dotnet-counters` on fresh pods; most startup CPU was JIT.
Resolution
Published with `PublishReadyToRun` for `linux-x64`, kept tiering on, and verified steady-state throughput in a load test.
Prevention
Added time-to-first-response to the deployment dashboard.

How to tell it (STAR)

During [sales events] our checkout service scaled out, but new pods were slow to warm up. I showed JIT was most of the startup cost and enabled ReadyToRun. Time to first response dropped from [X] to [Y] and the image grew by [Z MB], which we accepted.

↩ Usually asked after

More in CLR Fundamentals

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me