A large .NET application may take several seconds or even minutes to become ready after deployment. This can affect container deployments, application restarts, autoscaling, and recovery after an unexpected failure.
The problem is not always caused by slow application code. Startup may involve dependency injection, database connections, configuration loading, migrations, external API calls, cache initialization, and background service registration.
When these operations are performed in the wrong order or block application startup unnecessarily, the application takes longer to become available.
This article explains how to investigate slow startup in ASP.NET Core applications, identify expensive initialization operations, and improve startup performance without removing important checks.
Understand What Happens During Application Startup
Before optimizing startup, understand the work performed when an ASP.NET Core application launches.
A typical application performs several operations:
- Load configuration and environment variables.
- Register services in the dependency injection container.
- Configure middleware and routing.
- Initialize logging and monitoring.
- Establish database connections.
- Initialize caches and other application resources.
- Start hosted background services.
- Begin accepting HTTP requests.
Not all these operations happen at the same time.
For example, registering a database service with dependency injection does not necessarily establish a database connection immediately. The connection may be created when the service is first resolved.
Similarly, a hosted service can delay application startup if its StartAsync method performs lengthy operations before returning.
The first step is to identify which operations actually contribute to the startup delay.
Measure Startup Time Instead of Guessing
Start by recording timestamps around important initialization operations.
A simple timing helper can measure how long each operation takes.
public class StartupTimer
{
private readonly ILogger<StartupTimer> _logger;
public StartupTimer(
ILogger<StartupTimer> logger)
{
_logger = logger;
}
public async Task MeasureAsync(
string operationName,
Func<Task> operation)
{
var stopwatch = Stopwatch.StartNew();
try
{
await operation();
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Startup operation {Operation} took {DurationMs} ms",
operationName,
stopwatch.ElapsedMilliseconds);
}
}
}Use this helper around expensive initialization tasks.
For example:
await startupTimer.MeasureAsync(
"DatabaseConnectionCheck",
async () =>
{
await dbContext.Database.CanConnectAsync();
});The measurement identifies how much time the database connection check consumes.
In production, record startup duration consistently across deployments. This helps determine whether a performance problem is related to a new release, infrastructure changes, or an external dependency.
Also distinguish between process startup, application initialization, and readiness. A process can be running while the application is still unable to serve useful requests.
Check Dependency Injection Initialization
Dependency injection is a common source of unexpected startup delays.
Consider a service registered as a singleton:
builder.Services.AddSingleton<ReportService>();If its constructor performs expensive work, resolving that service can become slow.
For example:
public class ReportService
{
public ReportService()
{
LoadLargeReportTemplates();
ConnectToExternalService();
}
private void LoadLargeReportTemplates()
{
// Load and process templates
}
private void ConnectToExternalService()
{
// Establish external connection
}
}This design mixes object construction with resource initialization.
It also makes the service difficult to test because simply creating an instance triggers external operations.
Keep constructors lightweight. Move asynchronous initialization into an explicit method or a hosted initialization component.
public class ReportService
{
public ReportService()
{
// Assign dependencies only
}
public async Task InitializeAsync(
CancellationToken cancellationToken)
{
await LoadTemplatesAsync(cancellationToken);
}
private Task LoadTemplatesAsync(
CancellationToken cancellationToken)
{
// Load templates
return Task.CompletedTask;
}
}This gives the application control over when initialization happens.
It also makes it possible to measure the operation separately.
Investigate Database Startup Delays
Database initialization can add significant time to application startup.
Common causes include:
- Slow DNS resolution.
- Network connectivity problems.
- Database server startup.
- Authentication failures.
- Connection pool initialization.
- Expensive schema migrations.
- Large database queries during initialization.
A basic connectivity check can help identify connection problems:
public class DatabaseInitializer
{
private readonly AppDbContext _context;
private readonly ILogger<DatabaseInitializer> _logger;
public DatabaseInitializer(
AppDbContext context,
ILogger<DatabaseInitializer> logger)
{
_context = context;
_logger = logger;
}
public async Task CheckConnectionAsync(
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var connected = await _context.Database
.CanConnectAsync(cancellationToken);
stopwatch.Stop();
_logger.LogInformation(
"Database connection check completed. " +
"Connected: {Connected}, Duration: {DurationMs} ms",
connected,
stopwatch.ElapsedMilliseconds);
if (!connected)
{
throw new InvalidOperationException(
"Database is not available.");
}
}
}A database connection check is useful when the application cannot function without the database.
However, do not automatically run expensive queries or migrations every time an application instance starts.
For production deployments, database migrations are often better handled as a separate deployment step or migration job. This avoids multiple application instances attempting schema changes simultaneously.
Move Nonessential Work Out of the Startup Path
Not every initialization operation needs to finish before the application accepts requests.
For example, a reporting application might load a large historical dataset into memory before becoming available.
Join the conversation! Your thoughts help the community grow.