In modern web applications, some operations don’t need to be executed immediately within a user request. Examples include:
ASP.NET Core provides a built-in solution for these scenarios: Hosted Services. Hosted services allow you to run background tasks independently of HTTP requests in a clean, reliable way.
This article explains how to implement background tasks using hosted services, with practical examples for full-stack applications.
What You Will Learn
What hosted services are and why they are useful
Types of hosted services in ASP.NET Core
Implementing IHostedService for custom background tasks
Using BackgroundService for recurring jobs
Scheduling tasks and cancellation handling
Best practices for production-ready background processing
Part 1: Understanding Hosted Services
A hosted service is a class that runs in the background of your ASP.NET Core application. It starts when the application starts and stops when the application shuts down.
Types:
IHostedService: Basic interface for background services.
BackgroundService: Abstract class implementing IHostedService with a convenient ExecuteAsync method for long-running tasks.
Part 2. Creating a simple hosted service
Step 1: Create a Class Implementing BackgroundService
Services/EmailSenderService.cs
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
public class EmailSenderService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Replace with your background logic
Console.WriteLine($"Sending emails at {DateTime.Now}");
// Wait for 1 minute before running again
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Step 2: Register Hosted Service
In Program.cs:
builder.Services.AddHostedService<EmailSenderService>();
Now, when your application starts, EmailSenderService runs in the background automatically.
Part 3. Using Dependency Injection in hosted services
Hosted services can use services like DbContext, email service, or logging via dependency injection.
public class CleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<CleanupService> _logger;
public CleanupService(IServiceScopeFactory scopeFactory, ILogger<CleanupService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var expiredItems = db.Tokens.Where(t => t.Expiry < DateTime.Now);
db.Tokens.RemoveRange(expiredItems);
await db.SaveChangesAsync();
_logger.LogInformation("Expired tokens cleaned at {time}", DateTime.Now);
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
Here, IServiceScopeFactory ensures proper lifetime management of scoped services like DbContext.
Part 4. Scheduling Background Tasks
For recurring tasks, you can use Task.Delay inside ExecuteAsync. For more advanced scheduling, consider:
Example using a Timer:
public class TimerService : IHostedService, IDisposable
{
private Timer _timer;
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(30));
return Task.CompletedTask;
}
private void DoWork(object state)
{
Console.WriteLine($"Task executed at {DateTime.Now}");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose() => _timer?.Dispose();
}
Part 5. Integrating with email Service (Real-world Example)
Suppose you want to send queued emails in the background:
public class QueuedEmailService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public QueuedEmailService(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>();
var emailService = scope.ServiceProvider.GetRequiredService<EmailService>();
var pendingEmails = db.EmailQueue.Where(e => !e.Sent);
foreach (var email in pendingEmails)
{
await emailService.SendEmailAsync(email);
email.Sent = true;
}
await db.SaveChangesAsync();
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
This pattern ensures emails are sent without blocking user requests.
Part 6. Stopping Background Taska
CancellationToken stoppingToken is automatically triggered when:
Always check the token in long-running loops:
while (!stoppingToken.IsCancellationRequested)
{
// Your logic
}
Part 7. Best Practices
Use IServiceScopeFactory for scoped services (like DbContext)
Handle exceptions – log errors without crashing the service
Avoid blocking calls – always use async/await
Use Task.Delay instead of Thread.Sleep
Keep tasks lightweight – offload heavy processing to external queues if needed
Graceful shutdown – always respect stoppingToken
Part 8. Monitoring and Scaling
Use application logs to monitor background tasks
Combine with health checks to ensure background services are running
For heavy workloads, consider queue-based processing with Hangfire, RabbitMQ, or Azure Functions
Hosted services scale automatically with the application instance, so tasks run per server instance
Conclusion
Hosted services in ASP.NET Core allow you to run background tasks safely and efficiently. Key takeaways:
BackgroundService and IHostedService are the foundation
Proper dependency injection and cancellation handling is crucial
Can be used for emails, cleanup tasks, reporting, or queue processing
Combine with health checks and logging for production-ready applications
By implementing background tasks with hosted services, you can improve performance, user experience, and reliability of your full-stack applications.