Improving .NET cold starts with eager initialization tasks
, by Catalin Gavan
TL;DR
The first request in a .NET/C# application is significantly slower than the subsequent runs, because the Just-In-Time (JIT) compiler converts assemblies (.dll files) into machine code only when they are first used.
By creating a simple IApplicationInitializer interface, you can run important tasks and operations directly during application startup (inside Program.cs) instead of waiting for the first incoming request.
Eager running initialization services would also trigger JIT compilation early, helping your application respond more consistently.
Some common reasons why a .NET application can be slow on first run:
- Assemblies (.dll files) are compiled to machine code by the JIT compiler only when first called.
- Some libraries or services use
Lazy<>initialization, which only runs on first access. - The application might fetch data from external services, which can be time-consuming, or those services might themselves be "in a cold state".
- Time-consuming database queries, like loading refresh tokens, configuration, or caching large collections, are often done on first request.
Most of these operations are expensive but usually need to run only once.
For web applications, this can be even more problematic, as two concurrent requests could trigger the same slow initialization operations simultaneously.
Running these operations eagerly at startup - essentially executing them before the first request - helps reduce cold-start delays and ensures a more consistent application behavior.
Goals
I wanted a solution that:
- Works with .NET dependency injection
- Is asynchronous
- Allows ordering, so some tasks can run after others
The interface
I created a simple interface for executing application startup tasks:
public interface IApplicationInitializer
{
int Order { get; }
Task InitializeAsync();
}
Initializers
TenantConfigurationInitializer
A common use case of pre-loading data from a database and saving it into a cache layer for more efficient access.
internal class TenantConfigurationInitializer : IApplicationInitializer
{
private readonly AppDbContext _dbContext;
private readonly ICacheManager _cacheManager;
private readonly ILogger<TenantConfigurationInitializer> _logger;
public TenantConfigurationInitializer(
AppDbContext dbContext,
ICacheManager cacheManager,
ILogger<TenantConfigurationInitializer> logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public int Order => 5;
public async Task InitializeAsync()
{
_logger.LogInformation("Preloading tenants...");
var tenants = await _dbContext.Tenants.AsNoTracking().ToListAsync();
foreach (var tenant in tenants)
{
// eager save the tenant into the cache layer
_cacheManager.Set($"tenant_{tenant.Id}", tenant, TimeSpan.FromHours(1));
}
_logger.LogInformation("Cached {Count} tenants.", tenants.Count);
}
}
LocalizationInitializer
An example of loading data from an external service, such as localization labels.
internal class LocalizationInitializer : IApplicationInitializer
{
private readonly HttpClient _client;
private readonly ILocalizationProvider _localizationProvider;
public LocalizationInitializer(
IHttpClientFactory clientFactory,
ILocalizationProvider localizationProvider)
{
_client = clientFactory?.CreateClient() ?? throw new ArgumentNullException(nameof(clientFactory));
_localizationProvider = localizationProvider ?? throw new ArgumentNullException(nameof(localizationProvider));
}
public int Order => 20;
public async Task InitializeAsync()
{
using var response = await _client.GetAsync("https://api.example.com/localization/en.json");
if(response.IsSuccessStatusCode)
{
string responseAsString = await response.Content.ReadAsStringAsync();
var labels = JsonSerializer.Deserialize<List<Localization.Label>>(responseAsString);
_localizationProvider.SetLabels("en", labels);
}
}
}
Registering and executing initializers
In Program.cs, register your initializers as transient services:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); // other application initializers can be registered builder.Services.AddTransient<IApplicationInitializer, TenantConfigurationInitializer>(); builder.Services.AddTransient<IApplicationInitializer, LocalizationInitializer>();
Then, during application startup, resolve and execute all initializers in order:
var builder = WebApplication.CreateBuilder(args);
// application services...
var app = builder.Build();
// Eagerly execute the application initializers
using (var scope = app.Services.CreateScope())
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
// resolve all the IApplicationInitializer implementations
// notice that Dependency Injection is resolved automatically
var initializers = scope.ServiceProvider
.GetServices<IApplicationInitializer>()
.OrderBy(p => p.Order).ToList();
foreach (var initializer in initializers)
{
// execute the custom IApplicationInitializer logic
await initializer.InitializeAsync();
}
logger.LogInformation("Application initializers completed.");
}
// Finally, start the application
app.Run();
Final thoughts
What makes this pattern useful:
- Separation of concerns – each initializer is responsible for a single task
- Extensibility – adding a new startup task is as easy as creating a new
IApplicationInitializer - Deterministic startup – the Order property ensures a predictable sequence
You can use this approach to warm up caches, load configurations, fetch API data, or initialize external services - all without cluttering Program.cs or affecting the initial user experience.