Efficient Background Jobs & Scheduled Tasks in .NET 8 with Hosted Services

In this post, we'll dive into how to run background jobs and schedule recurring tasks in .NET 8 using built-in Hosted Services. You’ll see: How to implement IHostedService and BackgroundService Strategies for scheduling (Timers, Cron) Real-world use cases (email sending, data cleanup, queue processing) Best practices for reliability, graceful shutdown, and observability Let’s get started! Table of Contents Introduction Understanding Hosted Services Creating a Simple BackgroundService Scheduling Recurring Tasks Handling One-Off Background Jobs Use Cases & Examples Best Practices Conclusion Introduction Background jobs and scheduled tasks are essential for: Processing messages or workqueues Periodic cleanup (logs, cache) Sending emails or notifications Data aggregation or reporting .NET 8’s Hosted Services (IHostedService/BackgroundService) provide a clean, DI-friendly way to run these tasks alongside your Web or API application. Understanding Hosted Services A Hosted Service is a class that runs in the background of your application. There are two main approaches: Implement IHostedService Two methods: StartAsync(CancellationToken) and StopAsync(CancellationToken). Derive from BackgroundService Override ExecuteAsync(CancellationToken) for a long-running loop. Both integrate with the Generic Host (WebApplication in minimal APIs or HostBuilder), support DI, configuration, and graceful shutdown. Creating a Simple BackgroundService Here's a minimal example of a service that logs a message every 10 seconds: using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; public class TimedLoggerService : BackgroundService { private readonly ILogger _logger; private readonly TimeSpan _interval = TimeSpan.FromSeconds(10); public TimedLoggerService(ILogger logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("TimedLoggerService started."); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("TimedLoggerService heartbeat at: {time}", DateTimeOffset.Now); await Task.Delay(_interval, stoppingToken); } _logger.LogInformation("TimedLoggerService stopping."); } } Register it in Program.cs: var builder = WebApplication.CreateBuilder(args); builder.Services.AddHostedService(); var app = builder.Build(); app.Run(); Scheduling Recurring Tasks Using PeriodicTimer In .NET 8 you can use PeriodicTimer: protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); while (await timer.WaitForNextTickAsync(stoppingToken)) { // Your scheduled work here } } Cron-Style Scheduling For cron expressions, use a library like Cronos: using Cronos; public class CronJobService : BackgroundService { private readonly CronExpression _expression = CronExpression.Parse("0 */5 * * * *"); // every 5 minutes private DateTimeOffset _nextRun = DateTimeOffset.MinValue; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var now = DateTimeOffset.Now; if (now >= _nextRun) { // Do work _nextRun = _expression.GetNextOccurrence(now) ?? DateTimeOffset.MaxValue; } await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } } Handling One-Off Background Jobs To enqueue fire-and-forget jobs (e.g., send an email after a request), use a Channel: public interface IBackgroundJobQueue { ValueTask QueueJobAsync(Func job); ValueTask DequeueAsync(CancellationToken cancellationToken); } public class BackgroundJobQueue : IBackgroundJobQueue { private readonly Channel _queue = Channel.CreateUnbounded(); public async ValueTask QueueJobAsync(Func job) => await _queue.Writer.WriteAsync(job); public async ValueTask DequeueAsync(CancellationToken token) => await _queue.Reader.ReadAsync(token); } public class QueuedHostedService : BackgroundService { private readonly IBackgroundJobQueue _jobQueue; private readonly ILogger _logger; public QueuedHostedService(IBackgroundJobQueue jobQueue, ILogger logger) { _jobQueue = jobQueue; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var job = await _jobQueue.DequeueAsync(stoppingToken); try { await job(stoppingToken); } catch (Exception ex) { _logger.Lo

Apr 26, 2025 - 16:29
 0
Efficient Background Jobs & Scheduled Tasks in .NET 8 with Hosted Services

In this post, we'll dive into how to run background jobs and schedule recurring tasks in .NET 8 using built-in Hosted Services. You’ll see:

  • How to implement IHostedService and BackgroundService
  • Strategies for scheduling (Timers, Cron)
  • Real-world use cases (email sending, data cleanup, queue processing)
  • Best practices for reliability, graceful shutdown, and observability

Let’s get started!

Table of Contents

  • Introduction
  • Understanding Hosted Services
  • Creating a Simple BackgroundService
  • Scheduling Recurring Tasks
  • Handling One-Off Background Jobs
  • Use Cases & Examples
  • Best Practices
  • Conclusion

Introduction

Background jobs and scheduled tasks are essential for:

  • Processing messages or workqueues
  • Periodic cleanup (logs, cache)
  • Sending emails or notifications
  • Data aggregation or reporting

.NET 8’s Hosted Services (IHostedService/BackgroundService) provide a clean, DI-friendly way to run these tasks alongside your Web or API application.

Understanding Hosted Services

A Hosted Service is a class that runs in the background of your application. There are two main approaches:

  • Implement IHostedService
    • Two methods: StartAsync(CancellationToken) and StopAsync(CancellationToken).
  • Derive from BackgroundService
    • Override ExecuteAsync(CancellationToken) for a long-running loop.

Both integrate with the Generic Host (WebApplication in minimal APIs or HostBuilder), support DI, configuration, and graceful shutdown.

Creating a Simple BackgroundService

Here's a minimal example of a service that logs a message every 10 seconds:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class TimedLoggerService : BackgroundService
{
    private readonly ILogger<TimedLoggerService> _logger;
    private readonly TimeSpan _interval = TimeSpan.FromSeconds(10);

    public TimedLoggerService(ILogger<TimedLoggerService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("TimedLoggerService started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("TimedLoggerService heartbeat at: {time}", DateTimeOffset.Now);
            await Task.Delay(_interval, stoppingToken);
        }

        _logger.LogInformation("TimedLoggerService stopping.");
    }
}

Register it in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<TimedLoggerService>();
var app = builder.Build();
app.Run();

Scheduling Recurring Tasks

Using PeriodicTimer

In .NET 8 you can use PeriodicTimer:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
    while (await timer.WaitForNextTickAsync(stoppingToken))
    {
        // Your scheduled work here
    }
}

Cron-Style Scheduling

For cron expressions, use a library like Cronos:

using Cronos;

public class CronJobService : BackgroundService
{
    private readonly CronExpression _expression = CronExpression.Parse("0 */5 * * * *"); // every 5 minutes
    private DateTimeOffset _nextRun = DateTimeOffset.MinValue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var now = DateTimeOffset.Now;
            if (now >= _nextRun)
            {
                // Do work
                _nextRun = _expression.GetNextOccurrence(now) ?? DateTimeOffset.MaxValue;
            }
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

Handling One-Off Background Jobs

To enqueue fire-and-forget jobs (e.g., send an email after a request), use a Channel:

public interface IBackgroundJobQueue
{
    ValueTask QueueJobAsync(Func<CancellationToken, Task> job);
    ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundJobQueue : IBackgroundJobQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue =
        Channel.CreateUnbounded<Func<CancellationToken, Task>>();

    public async ValueTask QueueJobAsync(Func<CancellationToken, Task> job) =>
        await _queue.Writer.WriteAsync(job);

    public async ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken token) =>
        await _queue.Reader.ReadAsync(token);
}

public class QueuedHostedService : BackgroundService
{
    private readonly IBackgroundJobQueue _jobQueue;
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundJobQueue jobQueue, ILogger<QueuedHostedService> logger)
    {
        _jobQueue = jobQueue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var job = await _jobQueue.DequeueAsync(stoppingToken);
            try { await job(stoppingToken); }
            catch (Exception ex) { _logger.LogError(ex, "Error executing job"); }
        }
    }
}

Register in Program.cs:

builder.Services.AddSingleton<IBackgroundJobQueue, BackgroundJobQueue>();
builder.Services.AddHostedService<QueuedHostedService>();

Use Cases & Examples

  • Email & Notification Sending: Offload slow SMTP calls.
  • Data Cleanup: Remove stale records or temp files on a schedule.
  • Message Processing: Consume a queue or topic (RabbitMQ, Azure Service Bus).
  • Report Generation: Build and store reports during off-peak hours.

Best Practices

  • Graceful Shutdown: Honor the CancellationToken and clean up resources.
  • Exception Handling: Catch exceptions inside loops to avoid crashing the service.
  • Logging & Metrics: Instrument your service with logs and counters (e.g., Prometheus).
  • Dependency Injection: Inject only scoped or singleton services appropriately.
  • Backoff & Retry: Implement retry policies with exponential backoff for transient failures.
  • Health Checks: Expose health endpoints to monitor background service status.

Conclusion

Hosted Services in .NET 8 provide a powerful and flexible way to run background jobs and scheduled tasks. By following these examples and best practices, you can build reliable, maintainable, and observable background processing in your applications.

Feel free to share your own experiences or questions in the comments below!

Happy coding!