Background Tasks & Hosted Services
15 questions found
What is a BackgroundService in ASP.NET Core, and how do you register one to run alongside a web application?
Beginner
BackgroundService is an abstract base class for long-running background work that runs within the same process as your web application, started automatically at application startup via AddHostedService<T>() and given a chance to shut down gracefully when the application stops -- you implement its ExecuteAsync(CancellationToken) method with your work loop.
public class QueueProcessor : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
await ProcessQueueAsync();
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
builder.Services.AddHostedService<QueueProcessor>();
Real-world example
A web API registers a BackgroundService that polls a message queue every few seconds and processes pending notifications, running continuously in the same process without needing a separately deployed worker service.
Common follow-ups: How does the host handle exceptions thrown inside ExecuteAsync?;What's the difference between BackgroundService and a dedicated Worker Service project?
Hosting Models: Kestrel
IIS & Reverse Proxies;Dependency Injection
How do you safely access scoped services like a DbContext from within a BackgroundService, which is registered as a singleton?
Intermediate
Since a BackgroundService instance is a singleton for the application's lifetime, you can't inject scoped services (like AppDbContext) directly into its constructor -- inject IServiceScopeFactory instead, and create a new scope for each unit of work, resolving scoped services from that scope and disposing it afterward.
public class OrderProcessor : BackgroundService {
private readonly IServiceScopeFactory _scopeFactory;
public OrderProcessor(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await ProcessPendingOrdersAsync(db);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Real-world example
A background order-processing service creates a fresh DI scope for every polling cycle, ensuring each cycle gets its own DbContext instance rather than sharing a single long-lived context across the entire application lifetime.
Common follow-ups: What exception occurs if you try injecting a scoped DbContext directly into a BackgroundService constructor?;How often should you create a new scope -- per iteration or per item processed?
Dependency Injection;Entity Framework Core & Data Access
How does graceful shutdown work for a BackgroundService, and how should ExecuteAsync respect the CancellationToken?
Advanced
When the host begins shutting down, it signals the CancellationToken passed to ExecuteAsync and gives the service a configurable grace period (ShutdownTimeout, default 30 seconds) to finish current work and exit cleanly before being forcibly terminated -- well-behaved implementations check the token frequently and pass it into any awaited operations (Task.Delay, HTTP calls, database queries) so cancellation propagates correctly rather than the service ignoring shutdown signals.
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
try {
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
} catch (OperationCanceledException) {
break; // expected during graceful shutdown
}
}
}
Real-world example
A containerized worker correctly handles Kubernetes' SIGTERM-then-SIGKILL shutdown sequence by respecting the CancellationToken throughout, finishing its current batch within the grace period instead of being abruptly killed mid-processing.
Common follow-ups: How do you configure the ShutdownTimeout grace period?;What happens if ExecuteAsync ignores the CancellationToken entirely?
Hosting Models: Kestrel
IIS & Reverse Proxies;Diagnostics & Performance
How should exceptions be handled inside a BackgroundService's work loop to prevent one failure from silently stopping all future processing?
Intermediate
An unhandled exception escaping ExecuteAsync stops the BackgroundService entirely (and depending on configuration, can bring down the whole host), so production code should wrap individual work iterations in try/catch, log the exception, and continue the loop -- reserving unhandled propagation only for genuinely fatal, unrecoverable conditions that should stop the application.
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
try {
await ProcessBatchAsync();
} catch (Exception ex) {
_logger.LogError(ex, "Batch processing failed, will retry next cycle");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
Real-world example
A background service silently stopped processing for days after an unhandled exception in one iteration terminated the entire loop; wrapping each iteration in try/catch ensures a single bad item no longer takes down the whole processor.
Common follow-ups: What is BackgroundServiceExceptionBehavior and how does it control host-crashing behavior?;How do you get alerted when a background service has unexpectedly stopped?
Logging;Health Checks
How would you implement a producer-consumer background processing pipeline using System.Threading.Channels alongside a BackgroundService?
Advanced
Channel<T> provides a high-performance, thread-safe async queue -- a producer (like an API endpoint) writes items via ChannelWriter.WriteAsync, while a BackgroundService acts as the consumer, reading items via ChannelReader.ReadAllAsync in a loop, decoupling request-handling latency from background processing time while a bounded channel provides built-in backpressure if the queue fills up.
var channel = Channel.CreateBounded<EmailMessage>(capacity: 100);
builder.Services.AddSingleton(channel.Writer);
builder.Services.AddSingleton(channel.Reader);
builder.Services.AddHostedService<EmailSenderService>();
public class EmailSenderService : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken ct) {
await foreach (var message in _reader.ReadAllAsync(ct)) {
await SendEmailAsync(message);
}
}
}
Real-world example
An API endpoint accepting bulk email requests writes each email to a bounded Channel and returns immediately (202 Accepted), while a background service consumes and actually sends the emails, keeping the API responsive under heavy load.
Common follow-ups: What happens when a bounded channel is full and a producer tries to write?;How does this compare to using an external message queue like RabbitMQ or Azure Service Bus?
Concurrency (asyncio/threading/multiprocessing);Rate Limiting
How do you implement a periodic scheduled task using PeriodicTimer instead of Task.Delay, and what problem does it solve?
Intermediate
PeriodicTimer (introduced in .NET 6) provides an async-friendly, precise interval timer via WaitForNextTickAsync(), avoiding the cumulative drift that naive Task.Delay-based loops can introduce (where each cycle's total duration equals work time plus delay time, drifting over many iterations) -- PeriodicTimer's ticks stay aligned to the original interval regardless of how long each cycle's work takes.
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(stoppingToken)) {
await RunScheduledJobAsync();
}
}
Real-world example
A metrics-collection background service switches from Task.Delay to PeriodicTimer to guarantee samples are taken at consistent 5-minute intervals regardless of how long each collection cycle takes, improving the consistency of trend analysis.
Common follow-ups: How does WaitForNextTickAsync behave if the previous tick's work exceeds the interval?;What happens to a PeriodicTimer when it's disposed mid-wait?
Diagnostics & Performance;Configuration & Options Pattern
How would you implement a cron-like scheduled background job (e.g., 'run daily at 2 AM') without a third-party scheduling library?
Advanced
Inside ExecuteAsync's loop, use a library like Cronos to parse a cron expression and compute the next occurrence time, then Task.Delay until that exact moment before running the job and recalculating the next occurrence -- for more complex or distributed scheduling needs (multiple instances, persistence, retries, UI for managing jobs), dedicated libraries like Hangfire or Quartz.NET are usually preferred over hand-rolled scheduling.
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
var schedule = CronExpression.Parse("0 2 * * *"); // 2 AM daily
while (!stoppingToken.IsCancellationRequested) {
var next = schedule.GetNextOccurrence(DateTimeOffset.UtcNow);
var delay = next.Value - DateTimeOffset.UtcNow;
await Task.Delay(delay, stoppingToken);
await RunNightlyReportAsync();
}
}
Real-world example
A reporting service runs a nightly summary job at exactly 2 AM using a cron expression parsed with Cronos, rather than hardcoding a fixed Task.Delay that would drift relative to wall-clock time across daylight saving transitions.
Common follow-ups: When should you reach for Hangfire or Quartz.NET instead of hand-rolling scheduling?;How do you handle a missed scheduled run if the application was down at the scheduled time?
Logging;Configuration & Options Pattern
How do multiple independently-registered BackgroundServices coexist within the same application, and does registration order matter?
Intermediate
Each BackgroundService is registered independently via AddHostedService<T>(), and the host starts each one's StartAsync sequentially in registration order (so a service registered first completes its StartAsync before the next one's begins), but their ExecuteAsync work loops then run concurrently, independently, for the rest of the application's lifetime -- they don't interfere with each other unless they explicitly share state or resources.
builder.Services.AddHostedService<DatabaseMigrationService>(); // StartAsync runs first, must complete
builder.Services.AddHostedService<QueueProcessor>(); // then this one starts
builder.Services.AddHostedService<CacheWarmupService>(); // then this one
// After startup, all three ExecuteAsync loops run concurrently
Real-world example
A team ensures database migrations complete before any other background work begins by registering a DatabaseMigrationService first, relying on the host's sequential StartAsync ordering to enforce this critical startup dependency.
Common follow-ups: What happens if one service's StartAsync throws an exception during startup?;How would you make services start concurrently instead of sequentially?
Entity Framework Core & Data Access;CI/CD
Publishing & Deployment
How do background services interact with health checks to signal their operational status to an orchestrator like Kubernetes?
Advanced
A background service can expose its health via a custom IHealthCheck that inspects shared state (like a 'last successful run' timestamp the service updates on each cycle), letting the health endpoint report unhealthy if the background work has stalled or crashed -- crucial because a web server can still respond to HTTP requests perfectly fine even while its background processing has silently died, so relying solely on basic liveness (process is running) wouldn't catch this failure mode.
public class QueueProcessorHealthCheck : IHealthCheck {
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct) {
var elapsed = DateTime.UtcNow - QueueProcessor.LastSuccessfulRun;
return Task.FromResult(elapsed < TimeSpan.FromMinutes(5)
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Queue processor stalled"));
}
}
builder.Services.AddHealthChecks().AddCheck<QueueProcessorHealthCheck>("queue-processor");
Real-world example
A Kubernetes liveness probe automatically restarts a pod whose background queue processor has been silently stuck for over 5 minutes, detected via a custom health check monitoring the processor's last successful iteration timestamp.
Common follow-ups: How do you distinguish liveness from readiness for a background-service-heavy application?;What's the risk of a health check that restarts too aggressively?
Health Checks;Hosting Models: Kestrel
IIS & Reverse Proxies
What is the difference between IHostedService and the BackgroundService convenience base class?
Intermediate
IHostedService is the low-level interface with just StartAsync and StopAsync methods, giving you full control for services that don't have an inherent continuous loop (like something that just registers/deregisters once at startup and shutdown). BackgroundService is a convenience base class implementing IHostedService specifically for the common 'continuous work loop' pattern, handling the StartAsync/StopAsync plumbing so you only write ExecuteAsync.
// Direct IHostedService: no continuous loop needed
public class MetricsRegistrationService : IHostedService {
public Task StartAsync(CancellationToken ct) {
MetricsRegistry.Register("my-service");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct) {
MetricsRegistry.Unregister("my-service");
return Task.CompletedTask;
}
}
Real-world example
A service that simply registers the application with a service discovery system at startup and deregisters at shutdown implements IHostedService directly, since it has no ongoing loop -- unlike a queue processor, which extends BackgroundService instead.
Common follow-ups: Why does BackgroundService exist if IHostedService already covers this case?;Can a single class implement both patterns for different concerns?
Hosting Models: Kestrel
IIS & Reverse Proxies;Dependency Injection