How to Implement Timeouts for Async Tasks in C
In modern C# development, preventing asynchronous operations from
hanging indefinitely is critical for building resilient applications.
This article provides a straightforward guide on how to implement
timeouts for asynchronous tasks using
CancellationTokenSource and Task.Delay. You
will learn the recommended approach for operations that natively accept
cancellation tokens, as well as an alternative pattern for legacy or
non-cancellable operations using Task.WhenAny.
Approach
1: Using CancellationTokenSource with a Time Limit
The cleanest and most idiomatic way to implement a timeout in .NET is
to use a CancellationTokenSource configured with a specific
duration. This pattern works with any asynchronous method that accepts a
CancellationToken.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class TimeoutExample
{
private static readonly HttpClient httpClient = new HttpClient();
public static async Task FetchDataWithTimeoutAsync()
{
// Set a timeout of 5 seconds
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
HttpResponseMessage response = await httpClient.GetAsync("https://example.com", cts.Token);
string content = await response.Content.ReadAsStringAsync(cts.Token);
Console.WriteLine(content);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
Console.WriteLine("The operation timed out.");
}
}
}How It Works
CancellationTokenSource(TimeSpan)automatically triggers cancellation after the specified time passes.- You pass
cts.Tokento the asynchronous operation. - If the operation exceeds the deadline, the token signals
cancellation, causing the method to throw an
OperationCanceledException(orTaskCanceledException). - The
when (cts.IsCancellationRequested)filter ensures that you only treat the error as a timeout if the cancellation was initiated by your source rather than an external token.
Approach 2: Using
Task.WhenAny and Task.Delay
When working with third-party libraries or legacy code that does not
accept a CancellationToken, you can implement a timeout by
racing your task against a Task.Delay.
using System;
using System.Threading;
using System.Threading.Tasks;
public class TaskDelayTimeoutExample
{
public static async Task<TResult> WithTimeout<TResult>(Task<TResult> task, TimeSpan timeout)
{
using var timeoutCts = new CancellationTokenSource();
Task delayTask = Task.Delay(timeout, timeoutCts.Token);
Task completedTask = await Task.WhenAny(task, delayTask);
if (completedTask == task)
{
// Cancel the delay task to free up timer resources immediately
timeoutCts.Cancel();
return await task; // Propagates the result or original exception
}
throw new TimeoutException($"The operation exceeded the timeout of {timeout.TotalSeconds} seconds.");
}
}Usage
try
{
// Simulate a third-party non-cancellable task
Task<string> slowTask = LegacyLongRunningMethodAsync();
string result = await TaskDelayTimeoutExample.WithTimeout(slowTask, TimeSpan.FromSeconds(3));
Console.WriteLine(result);
}
catch (TimeoutException ex)
{
Console.WriteLine(ex.Message);
}Key Considerations for
Task.WhenAny
- Resource Management: Always cancel the
Task.Delayusing its ownCancellationTokenSourceonce the primary task finishes. If omitted, the background timer will run until the full timeout duration elapses, which can degrade performance under heavy load. - Unobserved Background Work: If the timeout fires first, the original task will continue executing in the background until completion because it cannot be cancelled. Ensure that background execution will not cause data corruption or resource exhaustion.
Alternative: .NET 6+
Task.WaitAsync
If you are using .NET 6 or later, .NET includes a built-in method
called WaitAsync that simplifies the
Task.WhenAny pattern for single tasks:
try
{
Task slowTask = LegacyLongRunningMethodAsync();
await slowTask.WaitAsync(TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
Console.WriteLine("The task timed out via WaitAsync.");
}WaitAsync accepts either a TimeSpan or a
CancellationToken directly, combining simplicity with
optimized resource handling. Use WaitAsync whenever
targeting modern .NET runtimes.