Background services play a critical role in modern applications. They process message queues, synchronize data, send notifications, generate reports, clean temporary files, monitor systems, and execute scheduled jobs independently of user requests.
In production environments, however, background services face many challenges. Network interruptions, database outages, temporary API failures, memory pressure, and unexpected exceptions can cause a service to stop processing work.
A self-healing background service is designed to detect failures, recover automatically, and continue processing without requiring manual intervention. Instead of simply restarting after an error, it incorporates resilience patterns such as retries, health monitoring, circuit breakers, graceful shutdown, and observability.
In this article, you'll learn how to build resilient background services in ASP.NET Core using production-oriented design patterns and best practices.
What Is a Background Service?
ASP.NET Core provides the BackgroundService base class for implementing long-running tasks.
Typical use cases include:
Processing message queues
Scheduled maintenance jobs
Email delivery
Data synchronization
Cache refresh
File processing
Event processing
Monitoring external systems
Unlike HTTP requests, background services execute continuously until the application shuts down.
Why Self-Healing Matters
Production services encounter failures that should not necessarily stop processing.
Common examples include:
Temporary database outages
Network interruptions
Cloud service throttling
External API failures
Queue connection loss
DNS resolution failures
Without recovery mechanisms, a single exception could stop the service permanently.
Typical Architecture
Hosted Service
│
Work Queue
│
Business Logic
│
External Services
│
Database
A resilient implementation surrounds each layer with appropriate error handling and monitoring.
Creating a Background Service
ASP.NET Core provides an abstract base class for hosted services.
public class WorkerService : BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Process work
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}
The cancellation token allows the service to stop gracefully during application shutdown.
Registering the Service
Register the worker during application startup.
builder.Services.AddHostedService<WorkerService>();
Hosted services start automatically when the application starts.
Handling Exceptions Properly
A common mistake is allowing exceptions to escape the processing loop.
Instead:
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessWorkAsync();
}
catch (Exception ex)
{
logger.LogError(ex,
"Processing failed.");
}
}
Catching exceptions prevents the processing loop from terminating unexpectedly.
Avoid catching exceptions without logging or handling them appropriately, as this can hide operational issues.
Implementing Retry Logic
Transient failures often succeed when retried after a short delay.
Examples include:
Temporary network failures
Database timeouts
Cloud service throttling
HTTP 429 responses
Pseudo-code:
await retryPolicy.ExecuteAsync(async () =>
{
await ProcessWorkAsync();
});
Only retry transient failures. Validation errors or malformed input should generally not be retried.
Using Exponential Backoff
Retrying immediately can increase pressure on already struggling systems.
Instead, increase the delay between attempts.
Example progression:
Attempt 1 → 1 second
Attempt 2 → 2 seconds
Attempt 3 → 4 seconds
Attempt 4 → 8 seconds
Exponential backoff reduces unnecessary load while allowing dependent services time to recover.
Implementing Health Checks
Health checks help determine whether the service is functioning correctly.
Example registration:
builder.Services.AddHealthChecks();
A health endpoint can expose the application's operational status to monitoring systems and orchestration platforms.
Health checks should verify more than process availability. They may include connectivity to databases, queues, or other critical dependencies.
Graceful Shutdown
Applications should stop cleanly without losing work.
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessWorkAsync();
}
}
Respecting the cancellation token allows the service to finish or safely stop ongoing operations during shutdown.
Processing Queued Work
A producer-consumer pattern is common in background processing.
Client
│
Queue
│
Background Service
│
Business Logic
Separating producers from consumers improves scalability and resilience.
Monitoring Service Activity
Track operational metrics such as:
Processed items
Failed items
Retry count
Queue length
Processing duration
Average latency
These metrics help detect issues before they affect users.
Logging Best Practices
Useful log information includes:
Correlation ID
Processing duration
Retry attempts
Exception details
Queue identifiers
Processing status
Join the conversation! Your thoughts help the community grow.