100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET

Worker Services

Learn how to build long-running background processes in .NET using the Worker Service template, BackgroundService, and the generic host.

Application TypesIntermediate9 min readJul 10, 2026
Analogies

What Is a Worker Service

A Worker Service is a .NET project template (dotnet new worker) purpose-built for long-running background processes with no UI — message queue consumers, scheduled data processors, or IoT device listeners. It's built on the same generic host (Host.CreateApplicationBuilder) that ASP.NET Core uses, so it gets dependency injection, configuration binding, and logging for free, and the actual work runs inside one or more classes implementing IHostedService, most commonly by inheriting the abstract BackgroundService base class.

🏏

Cricket analogy: A Worker Service is like a groundskeeper who works continuously in the background — mowing, rolling the pitch, maintaining the outfield — never appearing on the scorecard themselves but essential to every match (application) that depends on the ground being ready.

Building a Worker with BackgroundService

A class inheriting BackgroundService overrides ExecuteAsync(CancellationToken stoppingToken), typically wrapping the ongoing work in a loop that checks stoppingToken.IsCancellationRequested (or uses while (!stoppingToken.IsCancellationRequested)), so the host can signal a clean shutdown. Since .NET 8, PeriodicTimer is the recommended way to run work on an interval instead of Task.Delay in a loop, because it avoids drift from execution time and integrates cleanly with cancellation via WaitForNextTickAsync(stoppingToken).

🏏

Cricket analogy: A while loop checking stoppingToken each iteration is like a bowler checking with the umpire before every over whether time (or overs) remain — the moment the signal says stop, the spell ends cleanly rather than mid-delivery.

Dependency Injection and Configuration in Workers

BackgroundService implementations are registered as singletons (builder.Services.AddHostedService<Worker>()), so if the work loop needs a scoped service like a DbContext, it must create a scope explicitly via an injected IServiceScopeFactory inside ExecuteAsync, rather than injecting the scoped service directly into the worker's constructor. Configuration flows in the same way as any generic-host app — builder.Configuration binds appsettings.json, environment variables, and (in containers) environment-injected secrets into strongly typed options classes via IOptions<T>.

🏏

Cricket analogy: Creating a scope per loop iteration via IServiceScopeFactory is like a franchise fielding a fresh substitute for a single over under concussion-substitute rules — a short-lived participant brought in for one specific unit of work, then released, rather than staying on the field permanently like the singleton captain.

Deploying Worker Services

On Windows, calling builder.Services.AddWindowsService() (from the Microsoft.Extensions.Hosting.WindowsServices package) lets the same worker run as a Windows Service, managed with sc.exe or the Services console. On Linux, builder.Services.AddSystemd() integrates with systemd, supporting systemctl start/stop/status and journal logging, and this cross-platform host abstraction means the same worker code deploys unchanged as a Windows Service, a systemd unit, or inside a Docker container as the container's own long-running process.

🏏

Cricket analogy: The same worker deploying as a Windows Service or a systemd unit is like a bowler who's equally effective bowling on a green English pitch or a dry Indian one — the core skill (worker logic) travels, only the surrounding conditions (host platform) differ.

csharp
// Worker.cs
public sealed class DataSyncWorker : BackgroundService
{
    private readonly ILogger<DataSyncWorker> _logger;
    private readonly IServiceScopeFactory _scopeFactory;

    public DataSyncWorker(ILogger<DataSyncWorker> logger, IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

        while (!stoppingToken.IsCancellationRequested
               && await timer.WaitForNextTickAsync(stoppingToken))
        {
            using IServiceScope scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

            _logger.LogInformation("Sync run started at {Time}", DateTimeOffset.UtcNow);
            var pending = await db.SyncQueue.Where(x => !x.Processed).ToListAsync(stoppingToken);
            foreach (var item in pending)
            {
                item.Processed = true;
            }
            await db.SaveChangesAsync(stoppingToken);
        }
    }
}

// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddHostedService<DataSyncWorker>();
builder.Services.AddWindowsService();
builder.Services.AddSystemd();
var host = builder.Build();
host.Run();

PeriodicTimer.WaitForNextTickAsync respects the passed CancellationToken and won't drift the way a naive while(true) { DoWork(); await Task.Delay(interval); } loop can when DoWork's execution time varies — it always waits for the next scheduled tick, not the next tick after work finishes.

Never inject a scoped service like a DbContext directly into a BackgroundService's constructor — BackgroundServices are registered as singletons, and a scoped service held for the app's entire lifetime will hold stale connections and cause threading issues. Always resolve scoped dependencies through IServiceScopeFactory inside ExecuteAsync.

  • Worker Services (dotnet new worker) are built on the generic host, providing DI, configuration, and logging for background processes with no UI.
  • BackgroundService is the standard base class; override ExecuteAsync(CancellationToken) to implement the long-running work loop.
  • PeriodicTimer (recommended since .NET 8) avoids the timing drift of a Task.Delay-in-a-loop pattern for interval-based work.
  • BackgroundServices are registered as singletons; scoped dependencies like DbContext must be resolved via IServiceScopeFactory inside the loop, not injected directly.
  • AddWindowsService() and AddSystemd() let the same worker code run as a native Windows Service or systemd unit respectively.
  • The same worker binary can also run as the primary process inside a Docker container without code changes.
  • IOptions<T> binds appsettings.json, environment variables, and secrets into strongly typed configuration classes, same as any generic-host app.

Practice what you learned

Was this page helpful?

Topics covered

#NET#NETCoreStudyNotes#MicrosoftTechnologies#WorkerServices#Worker#Services#Service#Building#StudyNotes#SkillVeris