The Error
TaskCanceledException: A task was canceled
// Or: BackgroundService not stopping gracefully on app shutdown
Quick Fix - 2 Minutes
// Dev Fix: Proper BackgroundService pattern.NET 8
public class Worker : BackgroundService
{
private readonly ILogger _logger;
public Worker(ILogger<Worker> logger) => _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested) // Dev Fix: Check token
{
try
{
await DoWorkAsync(stoppingToken); // Pass token down
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); // Dev Fix: Delay respects cancellation
}
catch (OperationCanceledException)
{
// Dev Fix: Expected on shutdown, don't log as error
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in worker");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // Backoff
}
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker stopping"); // Dev Fix: Cleanup here
await base.StopAsync(cancellationToken);
}
}
Why This Happens
BackgroundService runs until app stops. If you don't check stoppingToken, Task.Delay throws TaskCanceledException..NET 8 Host will force-kill after 5 sec if you don't handle shutdown.
Real-World Scenario: Queue Processor + HttpClient
Most common production bug. Your worker calls an API but doesn't cancel the HttpClient:
public class QueueWorker : BackgroundService
{
private readonly IHttpClientFactory _httpFactory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var message = await GetNextMessage(stoppingToken);
// WRONG: No token passed to HttpClient
var client = _httpFactory.CreateClient();
await client.PostAsync("https://api.example.com/webhook", content);
// If app shuts down here, PostAsync hangs for 100s then throws TaskCanceledException
}
}
}
// FIX: Pass stoppingToken to every async call
var response = await client.PostAsync("https://api.example.com/webhook", content, stoppingToken);
// Now cancels in 5 seconds on shutdown instead of hanging
Why this matters: Without the token, K8s/Docker sends SIGTERM, your app waits 30s for HttpClient timeout, then SIGKILL drops the pod. You lose the message. With token, you exit cleanly in 5s.
Related Fixes You Should Know
BackgroundService issues cascade into:
- DbContext Was Disposed - BackgroundService is Singleton. Injecting DbContext directly causes disposal errors. Use
IServiceScopeFactory. - TaskCanceledException HttpClient - Same root cause. Forgetting to pass
CancellationTokento HttpClient makes shutdown take 100 seconds. - HostedService StartAsync Block - Doing long work in
StartAsyncwithout checking token freezes app startup. - HttpClient Socket Exhaustion - Creating
new HttpClient()in your worker loop exhausts sockets. Always useIHttpClientFactory.
FAQ
Q: Why do I get TaskCanceledException when stopping my app?
That's normal. When you shut down, .NET cancels the stoppingToken. Any Task.Delay or HttpClient call using that token throws OperationCanceledException. Catch it and exit gracefully.
Q: How long does BackgroundService wait to stop in.NET 8?
Default is 5 seconds. If your StopAsync doesn't finish, the host force-kills it. Set HostOptions.ShutdownTimeout to increase, but fix your code instead.
Best Practice for.NET 8
- Always pass
stoppingTokento delays + HTTP calls - Catch
OperationCanceledException- it's normal on shutdown - Override
StopAsyncfor cleanup: flush logs, close connections - For long work: split into chunks and check token between chunks
No comments yet. Be the first to share your thoughts!