LongRunning Tasks
Best Practices
Task Management
Software Development
Optimization Techniques

Best Practice LongRunning Task creation

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In .NET, a "long-running task" usually means work that should not compete with short request-handling tasks on the normal thread-pool path. The right design is less about forcing work into a task and more about deciding whether the work is CPU-bound, I/O-bound, recurring, or service-like.

The best practice is to choose a lifetime model first, then pick the task API. In many cases, async methods or hosted services are better than TaskCreationOptions.LongRunning.

Know When LongRunning Is Appropriate

TaskCreationOptions.LongRunning is a hint to the scheduler that the work may deserve its own dedicated thread. That can be useful for a worker loop that runs for a long time and does real synchronous work.

It is not a general performance switch. If the task mostly waits on I/O by using await, asking for LongRunning usually adds complexity without benefit.

This console example shows a dedicated worker that runs until cancellation:

csharp
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5var cts = new CancellationTokenSource();
6
7Task worker = Task.Factory.StartNew(() =>
8{
9    while (!cts.Token.IsCancellationRequested)
10    {
11        Console.WriteLine($"Working at {DateTime.UtcNow:O}");
12        Thread.Sleep(1000);
13    }
14}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
15
16await Task.Delay(3200);
17cts.Cancel();
18
19try
20{
21    await worker;
22}
23catch (OperationCanceledException)
24{
25    Console.WriteLine("Worker canceled");
26}

That pattern is reasonable when the loop is intentionally long-lived and does blocking work. It is a poor fit for ordinary request work or short jobs.

Prefer async for I/O-Bound Operations

If the task spends most of its time waiting on a database, HTTP call, file operation, or timer, write it as an async method instead of forcing a dedicated thread.

csharp
1using System;
2using System.Net.Http;
3using System.Threading;
4using System.Threading.Tasks;
5
6static async Task PollAsync(HttpClient client, CancellationToken token)
7{
8    while (!token.IsCancellationRequested)
9    {
10        string text = await client.GetStringAsync("https://example.com", token);
11        Console.WriteLine(text.Length);
12        await Task.Delay(TimeSpan.FromSeconds(30), token);
13    }
14}

This version scales better because the thread is returned to the runtime while the operation is awaiting external work. That is the key distinction: asynchronous waiting is not the same thing as a long-running CPU worker.

For Application Services, Prefer a Hosted Worker

Inside ASP.NET Core or a worker service, long-lived background logic usually belongs in BackgroundService instead of an ad hoc fire-and-forget task. A hosted service has explicit startup, shutdown, logging, and dependency injection boundaries.

csharp
1using Microsoft.Extensions.Hosting;
2using Microsoft.Extensions.Logging;
3
4public sealed class CleanupWorker : BackgroundService
5{
6    private readonly ILogger<CleanupWorker> _logger;
7
8    public CleanupWorker(ILogger<CleanupWorker> logger)
9    {
10        _logger = logger;
11    }
12
13    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
14    {
15        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
16
17        while (await timer.WaitForNextTickAsync(stoppingToken))
18        {
19            _logger.LogInformation("Running cleanup");
20            await Task.Delay(500, stoppingToken);
21        }
22    }
23}

This gives the runtime a clean way to stop the worker during application shutdown. It also avoids a common anti-pattern where code starts a task and then loses track of it.

Design for Cancellation and Observation

Whatever creation model you choose, treat cancellation and error observation as first-class concerns. Long-running work should have a CancellationToken, and exceptions should be awaited, logged, or otherwise observed.

If the task produces items for later processing, prefer a queue such as Channel rather than spinning up a new long-running task for each unit of work. One controlled worker is easier to reason about than many unmanaged ones.

Common Pitfalls

The most common mistake is using TaskCreationOptions.LongRunning for work that is actually asynchronous and mostly idle. That can waste threads and reduce scalability.

Another problem is creating fire-and-forget tasks inside web requests. If the request ends, the task may outlive the request scope, lose dependencies, or be terminated during app shutdown.

It is also easy to forget cancellation. A worker without a stop path becomes hard to shut down cleanly in tests, containers, and production.

Finally, developers sometimes assume "long-running" means "faster." It does not. It is only a scheduler hint, and the wrong hint can make the system worse.

Summary

  • Use LongRunning only for truly long-lived synchronous work.
  • Prefer async methods for I/O-bound operations.
  • In application code, hosted services are usually better than raw background tasks.
  • Always wire in cancellation and observe exceptions.
  • Do not create unmanaged fire-and-forget work unless you also own its lifetime.

Course illustration
Course illustration

All Rights Reserved.