Skip to content

Fix: C# TaskCanceledException: A task was canceled

FixDevs ·

Quick Answer

How to fix C# TaskCanceledException A task was canceled caused by HttpClient timeouts, CancellationToken, request cancellation, and Task.WhenAll failures.

The Error

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.TaskCanceledException
TaskCanceledException: 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 This Happens

In .NET, TaskCanceledException is thrown when:

  1. An HTTP request exceeds HttpClient.Timeout. The default timeout is 100 seconds.
  2. A CancellationToken is canceled. Code explicitly requested cancellation.
  3. The request is aborted. In ASP.NET, the client disconnected before the response was sent.
  4. Task.WhenAll propagates cancellation. One task cancels, affecting others.
  5. Deadlocks in async code. Blocking on async code with .Result or .Wait() can cause timeouts.

The most common cause in production applications is HTTP request timeouts from HttpClient.

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 seconds

Fixed — increase timeout:

var client = new HttpClient
{
    Timeout = TimeSpan.FromMinutes(5)
};
var response = await client.GetAsync("https://slow-api.example.com/large-report");

Fixed — 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");
    }
}

Pro Tip: Do not create new HttpClient() for every request. This causes socket exhaustion. Use IHttpClientFactory or a single static HttpClient instance. If you use a static instance, set the timeout once during initialization.

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");
}

Fix 3: Fix CancellationToken Usage

Passing a CancellationToken that gets canceled too early:

Broken — 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 — timeout 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 requests

Fixed — 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);

Common Mistake: Not disposing CancellationTokenSource. Always use using or call .Dispose(). Undisposed CancellationTokenSource objects with timeouts leak timer resources.

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 — deadlock in synchronous context:

// In a non-async method (e.g., MVC action filter, constructor)
var result = GetDataAsync().Result;  // DEADLOCK! Blocks the thread

Fixed — use 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);
}

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);
            }
        }
    }
}

Still Not Working?

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}");

For C# null reference exceptions, see Fix: C# System.NullReferenceException. For type conversion errors, see Fix: C# Cannot implicitly convert type.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles