Fix: C# TaskCanceledException: A task was canceled
Part of: C# and .NET Errors
Quick Answer
How to fix C# TaskCanceledException A task was canceled caused by HttpClient timeouts, CancellationToken, request cancellation, and Task.WhenAll failures.
A Task Was Canceled, But By What?
The first time this one bit me, a perfectly healthy API call started throwing TaskCanceledException in production and nowhere else. I wasted an afternoon hunting a bug in my own cancellation code before realizing the truth: nothing in my code canceled anything. HttpClient had quietly timed out, and .NET reports that with the exact same exception you get when a user navigates away. Ever since, the first question I ask when I see this is not “where is my bug” but “who actually pulled the trigger.” That single distinction is most of the fix.
Your C# application throws:
System.Threading.Tasks.TaskCanceledException: A task was canceled.Or variations:
System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.System.OperationCanceledException: The operation was canceled.System.Net.Http.HttpRequestException: The request was canceled due to the configured HttpClient.Timeout
---> System.Threading.Tasks.TaskCanceledExceptionTaskCanceledException: A task was canceled.
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)An asynchronous operation was canceled before it completed. This usually means an HTTP request timed out, a CancellationToken was triggered, or a task was explicitly canceled.
Why a Task Gets Canceled
TaskCanceledException is not really an error in the normal sense. It is the .NET runtime’s way of telling you that an awaited operation stopped before it produced a result because something asked it to stop. The exception derives from OperationCanceledException, and both carry a CancellationToken that identifies which cancellation source fired. The whole cooperative cancellation model in .NET runs on this: a token is handed down the call chain, some component signals it, and every await watching that token throws on its next checkpoint.
The confusing part is that two completely different situations surface as the same exception. The first is an explicit cancellation: your own code, a user action, or a host shutdown canceled a token on purpose. That is expected and you usually want to swallow it quietly. The second is a timeout: HttpClient keeps an internal CancellationTokenSource tied to its Timeout property, and when that fires it cancels the request with the exact same exception type. Telling these apart is the single most useful skill for debugging this error, and it is what Fix 2 below is about.
In .NET, TaskCanceledException is thrown when:
- An HTTP request exceeds
HttpClient.Timeout. The default timeout is 100 seconds. - A
CancellationTokenis canceled. Code explicitly requested cancellation. - The request is aborted. In ASP.NET, the client disconnected before the response was sent.
Task.WhenAllpropagates cancellation. One task cancels, affecting others.- Deadlocks in async code. Blocking on async code with
.Resultor.Wait()can cause timeouts.
The most common cause in production applications is HTTP request timeouts from HttpClient. The default 100-second window sounds generous, but it covers the entire request including DNS resolution, TCP connect, TLS handshake, sending the body, and reading the full response. A slow downstream service, a large file, or a stalled connection eats that budget quietly, and the exception only surfaces at the 100-second mark with no hint about which phase ran long.
Fix 1: Increase HttpClient Timeout
The default HttpClient.Timeout is 100 seconds. For slow APIs or large uploads:
Broken:
var client = new HttpClient();
var response = await client.GetAsync("https://slow-api.example.com/large-report");
// TaskCanceledException after 100 secondsThe fix, increase the timeout:
var client = new HttpClient
{
Timeout = TimeSpan.FromMinutes(5)
};
var response = await client.GetAsync("https://slow-api.example.com/large-report");Better, a per-request timeout using CancellationToken:
var client = new HttpClient
{
Timeout = Timeout.InfiniteTimeSpan // Disable global timeout
};
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var response = await client.GetAsync("https://slow-api.example.com/large-report", cts.Token);With IHttpClientFactory (recommended):
// In Program.cs or Startup.cs
builder.Services.AddHttpClient("SlowApi", client =>
{
client.BaseAddress = new Uri("https://slow-api.example.com/");
client.Timeout = TimeSpan.FromMinutes(5);
});
// In your service
public class MyService
{
private readonly HttpClient _client;
public MyService(IHttpClientFactory factory)
{
_client = factory.CreateClient("SlowApi");
}
}One thing I have learned the hard way: do not create new HttpClient() for every request. It looks harmless and it passes every test, then under load it exhausts sockets and you start seeing timeouts that have nothing to do with the remote server. Use IHttpClientFactory, or a single static HttpClient you set the timeout on once at startup.
Fix 2: Handle Timeouts Gracefully
Distinguish between timeouts and explicit cancellations:
try
{
var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient timeout
_logger.LogWarning("Request to {Url} timed out", url);
throw new TimeoutException($"Request to {url} timed out", ex);
}
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Explicit cancellation (user or application shutdown)
_logger.LogInformation("Request to {Url} was canceled", url);
throw;
}
catch (TaskCanceledException ex)
{
// Other cancellation
_logger.LogError(ex, "Request to {Url} was unexpectedly canceled", url);
throw;
}In .NET 6+, the distinction is clearer:
try
{
var response = await client.GetAsync(url, cancellationToken);
}
catch (TaskCanceledException ex) when (ex.CancellationToken != cancellationToken)
{
// Timeout — the HttpClient's internal token was canceled, not ours
Console.WriteLine("Request timed out");
}
catch (TaskCanceledException)
{
// Our token was canceled
Console.WriteLine("Request was explicitly canceled");
}The rule of thumb: compare ex.CancellationToken to the token you passed in. If they match, your code (or your caller) requested the cancellation and it should propagate normally. If they differ, the cancellation came from HttpClient’s internal timeout source, and you should treat it as a TimeoutException. Conflating the two is why so many apps log “task was canceled” with no idea whether a user navigated away or an API is down.
Fix 3: Fix CancellationToken Usage
Passing a CancellationToken that gets canceled too early:
Broken, the token cancels immediately:
using var cts = new CancellationTokenSource();
cts.Cancel(); // Canceled before the request even starts!
var response = await client.GetAsync(url, cts.Token);Broken, the timeout is too short:
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
var response = await client.GetAsync(url, cts.Token); // 100ms is too short for most HTTP requestsFixed with a reasonable timeout:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await client.GetAsync(url, cts.Token);Linking cancellation tokens:
// Combine a user cancellation token with a timeout
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, // From the caller
timeoutCts.Token // Our timeout
);
var response = await client.GetAsync(url, linkedCts.Token);A trap I have watched catch entire teams: forgetting to dispose the CancellationTokenSource. A source created with a timeout registers a timer, and if you never dispose it those timers pile up. Always wrap it in using or call .Dispose() yourself; on a hot path the leak is real.
Fix 4: Fix ASP.NET Request Cancellation
In ASP.NET, HttpContext.RequestAborted is triggered when the client disconnects:
[HttpGet("data")]
public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
// cancellationToken is automatically bound to HttpContext.RequestAborted
try
{
var data = await _service.GetDataAsync(cancellationToken);
return Ok(data);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Client disconnected — this is normal, not an error
_logger.LogDebug("Client disconnected during GetData");
return StatusCode(499); // Client Closed Request (non-standard)
}
}Pass the token through your entire call chain:
public async Task<Data> GetDataAsync(CancellationToken ct)
{
var dbResult = await _dbContext.Items
.Where(i => i.Active)
.ToListAsync(ct); // Pass token to EF Core
var apiResult = await _httpClient
.GetAsync("/api/external", ct); // Pass token to HttpClient
return new Data(dbResult, apiResult);
}Fix 5: Fix Retry Logic with Polly
Use Polly for structured retry and timeout policies:
using Polly;
using Polly.Extensions.Http;
// In Program.cs
builder.Services.AddHttpClient("MyApi")
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetTimeoutPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>() // Retry on timeout
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
static IAsyncPolicy<HttpResponseMessage> GetTimeoutPolicy()
{
return Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
}Simple retry without Polly:
async Task<HttpResponseMessage> GetWithRetry(string url, int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
return await _client.GetAsync(url);
}
catch (TaskCanceledException) when (i < maxRetries - 1)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
}
throw new TimeoutException($"Request to {url} failed after {maxRetries} retries");
}Fix 6: Fix Async Deadlocks
Blocking on async code causes timeouts that appear as TaskCanceledException:
Broken, a deadlock in a synchronous context:
// In a non-async method (e.g., MVC action filter, constructor)
var result = GetDataAsync().Result; // DEADLOCK! Blocks the threadFixed, go async all the way:
// Make the calling method async
var result = await GetDataAsync();If you must call async from sync (rare):
// Use Task.Run to avoid capturing the synchronization context
var result = Task.Run(() => GetDataAsync()).GetAwaiter().GetResult();Fixed, use .ConfigureAwait(false) in library code:
public async Task<string> GetDataAsync()
{
var response = await _client.GetAsync(url).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}Why deadlocks read as cancellations: when you block on async code with .Result or .Wait() inside a context that has a single-threaded synchronization context (classic ASP.NET, WPF, WinForms), the continuation cannot run because the one thread it needs is the thread you are blocking. The operation never completes, the request timer eventually fires, and you get a TaskCanceledException that looks like a network timeout but is really a self-inflicted thread starvation. ASP.NET Core does not install a synchronization context, so this specific deadlock is less common there, but Task.Run(...).GetAwaiter().GetResult() is still the only safe sync-over-async bridge when you genuinely cannot make the caller async.
Fix 7: Fix Task.WhenAll Cancellation
When one task in Task.WhenAll fails, the others may be canceled:
var tasks = new[]
{
client.GetAsync("https://api1.example.com/data"),
client.GetAsync("https://api2.example.com/data"),
client.GetAsync("https://api3.example.com/data"),
};
try
{
var results = await Task.WhenAll(tasks);
}
catch (TaskCanceledException)
{
// Which task was canceled?
foreach (var task in tasks)
{
if (task.IsCanceled)
Console.WriteLine("Task was canceled");
else if (task.IsFaulted)
Console.WriteLine($"Task faulted: {task.Exception?.InnerException?.Message}");
else
Console.WriteLine("Task completed successfully");
}
}Run tasks independently with error handling:
var results = await Task.WhenAll(
SafeGet(client, "https://api1.example.com/data"),
SafeGet(client, "https://api2.example.com/data"),
SafeGet(client, "https://api3.example.com/data")
);
async Task<string?> SafeGet(HttpClient client, string url)
{
try
{
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
catch (TaskCanceledException)
{
return null; // Return null instead of throwing
}
}Fix 8: Fix Background Service Cancellation
In hosted services, StoppingToken is canceled during shutdown:
public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Application is shutting down — this is expected
_logger.LogInformation("Service is stopping");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in background service");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
}Stranger Timeouts I’ve Chased
When the obvious fixes do not help, these are the less obvious causes I keep coming back to.
Check for DNS resolution timeouts. Slow DNS can cause the connection phase to timeout before the HTTP request even starts:
var handler = new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(5),
};
var client = new HttpClient(handler);Check for proxy issues. A corporate proxy might cause connection delays.
Enable HttpClient logging:
builder.Services.AddHttpClient("MyApi")
.ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(30); })
.AddHttpMessageHandler<LoggingHandler>();Check for thread pool starvation. If the thread pool is exhausted, tasks wait indefinitely:
ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
Console.WriteLine($"Available workers: {workerThreads}, IO: {completionPortThreads}");Thread pool starvation is the most under-diagnosed cause. The pool grows slowly (roughly one new thread per 500ms), so a burst of blocking .Result calls can leave dozens of requests queued behind a handful of threads. Every queued request races its own timeout, and the slowest ones lose. If GetAvailableThreads reports near-zero workers under load, the fix is upstream: remove the blocking calls, not raise the timeout.
Separate connect timeout from read timeout. HttpClient.Timeout is a single budget for the whole request. With SocketsHttpHandler you can split the phases so a dead host fails in seconds while a legitimately slow response still gets its full window:
var handler = new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(5),
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
};
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60),
};Streaming responses need HttpCompletionOption.ResponseHeadersRead. By default GetAsync buffers the entire response body before returning, so a large download counts against Timeout in full. For streaming or long downloads, return as soon as headers arrive and read the body without the global timeout pressing on it:
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
await using var stream = await response.Content.ReadAsStreamAsync(ct);Watch for cancellation that never reaches the awaited call. Passing a token to a method that ignores it does nothing. EF Core, Stream.ReadAsync, and most BCL async APIs accept a CancellationToken overload, and if you call the no-token overload, the operation cannot be canceled and your CancellationTokenSource timeout silently does nothing. Audit the call chain and make sure the token is threaded all the way down to the lowest async call.
For C# null reference exceptions, see Fix: C# NullReferenceException. For type conversion errors, see Fix: C# Cannot implicitly convert type.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: ASP.NET 500 Internal Server Error
Fix ASP.NET 500 Internal Server Error by enabling developer exception pages, fixing DI registration, connection strings, and middleware configuration.
Fix: C# async deadlock — Task.Result and .Wait() hanging forever
How to fix the C# async/await deadlock caused by Task.Result and .Wait() blocking the synchronization context in ASP.NET, WPF, WinForms, and library code.
Fix: C# Cannot implicitly convert type 'X' to 'Y'
How to fix C# cannot implicitly convert type error caused by type mismatches, nullable types, async return values, LINQ result types, and generic constraints.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.