Overview
Timeouts in C# are limits placed on how long an operation is allowed to run before the application stops waiting, cancels the work, or treats the operation as failed. They are commonly used for HTTP calls, database queries, background jobs, file operations, message processing, request handling, distributed service calls, and long-running asynchronous workflows.
Timeouts matter because production applications rarely run in perfect conditions. A remote API can become slow, a SQL query can block, a network call can hang, or a downstream dependency can degrade. Without timeouts, one slow dependency can consume threads, connections, memory, queue workers, and request capacity until the application becomes unstable.
In C#, timeouts are usually implemented through a combination of:
CancellationTokenSourceCancellationTokenCancelAfterTask.WaitAsync- API-specific timeout settings such as
HttpClient.Timeout - database command timeouts
- ASP.NET Core request timeout middleware
- resilience libraries that combine timeout, retry, circuit breaker, and rate limiting strategies
Timeouts are important in interviews because they test whether a developer understands real-world reliability, asynchronous programming, cancellation, resource protection, and distributed system behavior. A strong candidate should know that a timeout is not just a timer. It is a design decision about how long the caller is willing to wait, how cancellation is propagated, what exception is expected, how the operation is cleaned up, and how the system should respond when a dependency is too slow.
Core Concepts
What a Timeout Means
A timeout means the caller has decided that an operation has taken too long. The caller can then stop waiting, cancel the operation, return an error, retry, log the failure, or move the work to another path.
A timeout does not always mean the underlying operation has physically stopped. This distinction is very important.
For example:
await SomeOperationAsync().WaitAsync(TimeSpan.FromSeconds(2));
This limits how long the caller waits for SomeOperationAsync. If the timeout expires, the returned wait task fails with TimeoutException. However, the original operation may keep running unless that operation supports cancellation and receives a cancellation token.
A better pattern for cooperative cancellation is:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await SomeOperationAsync(cts.Token);
In this version, the timeout is represented as cancellation. The operation has a chance to stop itself when the token is canceled.
Timeout vs Cancellation
Timeout and cancellation are related but not identical.
A timeout is usually based on elapsed time. It means the operation exceeded a configured duration.
Cancellation is a cooperative signal that asks an operation to stop. Cancellation can be caused by a timeout, a user action, an HTTP client disconnect, application shutdown, or business logic.
Example:
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await service.ProcessAsync(timeoutCts.Token);
Here, timeout is implemented by canceling the token after five seconds.
In ASP.NET Core, cancellation often comes from the request:
app.MapGet("/orders/{id}", async (
int id,
OrderService service,
HttpContext context) =>
{
var order = await service.GetOrderAsync(id, context.RequestAborted);
return Results.Ok(order);
});
HttpContext.RequestAborted is canceled when the client disconnects or when request timeout middleware triggers a timeout. Passing it down prevents wasted work.
Cooperative Cancellation
C# cancellation is cooperative. The runtime does not safely kill arbitrary running code just because a token is canceled. Instead, methods that accept CancellationToken must check the token or pass it to lower-level APIs.
Common ways to observe cancellation:
public async Task ImportAsync(Stream stream, CancellationToken cancellationToken)
{
while (HasMoreRows(stream))
{
cancellationToken.ThrowIfCancellationRequested();
var row = await ReadRowAsync(stream, cancellationToken);
await SaveRowAsync(row, cancellationToken);
}
}
Key habits:
- Accept a
CancellationTokenin async methods that may take time. - Pass the token to every async operation that supports it.
- Use
ThrowIfCancellationRequestedin CPU-bound or loop-based work. - Do not swallow cancellation exceptions as normal failures.
- Avoid creating new tokens inside low-level methods unless you need a local timeout.
CancellationTokenSource.CancelAfter
CancellationTokenSource.CancelAfter schedules cancellation after a delay.
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
await DownloadFileAsync(url, cts.Token);
A shorter form is:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
await DownloadFileAsync(url, cts.Token);
CancelAfter is useful when an operation already accepts a CancellationToken. It creates a timeout by canceling the token after the configured duration.
A common mistake is creating a token but not passing it anywhere:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
await DownloadFileAsync(url); // The token is ignored.
The timeout has no effect unless the token is used by the operation.
Linked Cancellation Tokens
In real applications, an operation may need to stop for multiple reasons. For example, an API request should stop if:
- the client disconnects
- the endpoint-specific timeout expires
- the application is shutting down
Use CancellationTokenSource.CreateLinkedTokenSource to combine cancellation sources.
public async Task<OrderDto> GetOrderAsync(
int orderId,
CancellationToken requestAborted)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
requestAborted,
timeoutCts.Token);
return await _repository.GetOrderAsync(orderId, linkedCts.Token);
}
The linked token is canceled if either the original request token is canceled or the timeout token expires.
This is a common production pattern because it preserves caller cancellation while adding a local service-level timeout.
Task.WaitAsync
Task.WaitAsync can be used to wait for a task with a timeout.
var result = await GetReportAsync()
.WaitAsync(TimeSpan.FromSeconds(10));
If the task does not complete within the timeout, the wait fails with TimeoutException.
This is useful when the operation does not expose a cancellation token, or when the caller only wants to limit how long it waits.
However, WaitAsync does not automatically cancel the underlying work. The original task may continue running in the background.
A safer pattern is to combine timeout waiting with cancellation when the operation supports it:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var result = await GetReportAsync(cts.Token);
Use WaitAsync carefully for operations you do not control. It protects the caller from waiting forever, but it may not release the underlying resource unless the operation itself cooperates.
Task.Delay and Manual Timeout Patterns
Before Task.WaitAsync, developers often used Task.WhenAny with Task.Delay.
var operationTask = LoadDataAsync();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completed = await Task.WhenAny(operationTask, timeoutTask);
if (completed == timeoutTask)
{
throw new TimeoutException("Loading data timed out.");
}
var result = await operationTask;
This still appears in older codebases and is useful to understand in interviews. However, it has the same problem as WaitAsync: it only stops waiting. It does not necessarily cancel the original operation.
A better version uses cancellation:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result = await LoadDataAsync(cts.Token);
If you use Task.Delay, pass a token when appropriate:
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
HttpClient Timeouts
HttpClient has a Timeout property that applies a default timeout to requests made by that HttpClient instance.
builder.Services.AddHttpClient("OrdersClient", client =>
{
client.BaseAddress = new Uri("https://orders.example.com");
client.Timeout = TimeSpan.FromSeconds(10);
});
When the timeout is reached, the request task is canceled. The default HttpClient.Timeout is 100 seconds, which may be too long for many APIs.
For more fine-grained control, use a per-request cancellation token:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var response = await _httpClient.GetAsync(
"/api/orders/123",
cts.Token);
response.EnsureSuccessStatusCode();
You can also combine caller cancellation with a local timeout:
public async Task<string> GetInventoryAsync(CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
using var response = await _httpClient.GetAsync(
"/inventory",
linkedCts.Token);
return await response.Content.ReadAsStringAsync(linkedCts.Token);
}
Important HttpClient timeout habits:
- Do not create a new
HttpClientfor every request. - Prefer
IHttpClientFactoryin ASP.NET Core applications. - Set reasonable default timeouts for each named or typed client.
- Use per-request tokens when different operations need different limits.
- Understand that DNS lookup, connection establishment, request sending, response headers, and response body reading can have different timeout concerns.
- Consider
SocketsHttpHandler.ConnectTimeoutwhen connection establishment needs a separate limit.
Example with connection timeout:
builder.Services.AddHttpClient("ExternalApi")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(2)
});
Timeout Exceptions in HTTP Code
Timeouts can surface as different exception types depending on the API and configuration.
Common possibilities include:
OperationCanceledExceptionTaskCanceledExceptionTimeoutException- provider-specific exceptions
- resilience-library-specific exceptions
For example, HttpClient.Timeout commonly appears as task cancellation. Task.WaitAsync uses TimeoutException. SQL command timeout usually appears as a database provider exception.
A practical handler should distinguish cancellation from real errors:
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
return await client.GetStringAsync("/status", cts.Token);
}
catch (OperationCanceledException) when (!externalCancellationToken.IsCancellationRequested)
{
throw new TimeoutException("The downstream status API timed out.");
}
Be careful with this pattern. If you use linked tokens, you may need to check which token was canceled to decide whether it was caller cancellation or local timeout.
ASP.NET Core Request Timeouts
ASP.NET Core can apply request timeouts globally or per endpoint using request timeout middleware.
Example:
using Microsoft.AspNetCore.Http.Timeouts;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts(options =>
{
options.DefaultPolicy = new RequestTimeoutPolicy
{
Timeout = TimeSpan.FromSeconds(10),
TimeoutStatusCode = StatusCodes.Status504GatewayTimeout
};
});
var app = builder.Build();
app.UseRequestTimeouts();
app.MapGet("/slow-report", async (HttpContext context) =>
{
await GenerateReportAsync(context.RequestAborted);
return Results.Ok();
})
.WithRequestTimeout(TimeSpan.FromSeconds(5));
app.Run();
When the timeout is reached, HttpContext.RequestAborted is canceled. The application code should pass that token to downstream operations.
A request timeout does not mean every long-running endpoint should be forced into the same limit. Some endpoints need short limits, while streaming, file upload, WebSocket, and background job trigger endpoints may need different policies.
Database Command Timeouts
Database operations have their own timeout settings. In SQL Server, CommandTimeout controls how long a command waits before timing out. The default is commonly 30 seconds.
Example using ADO.NET:
using var command = connection.CreateCommand();
command.CommandText = "SELECT * FROM LargeReport WHERE TenantId = @TenantId";
command.CommandTimeout = 60;
using var reader = await command.ExecuteReaderAsync(cancellationToken);
In EF Core, command timeout can be configured in the provider options:
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.CommandTimeout(60);
});
});
A command timeout is not the same as a cancellation token:
- command timeout is provider/database-specific
- cancellation token is caller-driven cooperative cancellation
- both can be used together
Example:
var orders = await _dbContext.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync(cancellationToken);
In production code, avoid solving slow queries by blindly increasing command timeouts. First check indexing, query shape, blocking, transaction duration, missing filters, and data volume.
Timeouts in Background Services
Background workers should use timeouts to prevent one slow unit of work from blocking the entire worker loop.
public class InvoiceWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var itemTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
stoppingToken,
itemTimeout.Token);
try
{
await ProcessNextInvoiceAsync(linkedCts.Token);
}
catch (OperationCanceledException) when (itemTimeout.IsCancellationRequested)
{
// Log timeout and continue with the next item.
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
This protects the worker from being stuck forever on one message, file, or external call.
Timeout vs Retry
Timeouts and retries are often used together, but the order and total budget matter.
Bad pattern:
// 3 retries, each can wait 30 seconds.
// Total worst-case wait can be much longer than the caller expects.
Better thinking:
- Define a total operation budget.
- Define per-attempt timeout.
- Retry only safe operations.
- Avoid retrying non-idempotent writes unless the operation is designed for it.
- Add jittered backoff to avoid retry storms.
- Combine timeouts with circuit breakers for unhealthy dependencies.
Example idea:
builder.Services.AddHttpClient<OrderClient>()
.AddStandardResilienceHandler(options =>
{
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(15);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(3);
});
A total timeout limits the entire operation. An attempt timeout limits each individual try. This distinction is important in distributed systems interviews.
Timeout vs Circuit Breaker
A timeout protects one caller from waiting too long.
A circuit breaker protects the system from repeatedly calling a failing or overloaded dependency.
Example scenario:
- A payment API starts taking 20 seconds to respond.
- Timeout stops each request after 3 seconds.
- Retry may try again.
- Circuit breaker eventually stops sending more traffic temporarily.
Timeouts are usually the first layer of protection, but they are not enough by themselves. A resilient system often combines timeout, retry, circuit breaker, rate limiting, bulkhead isolation, fallback, and monitoring.
Timeout vs Deadline
A timeout is a duration from now.
A deadline is an absolute point in time by which the operation must finish.
Timeout example:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Deadline example:
var deadline = DateTimeOffset.UtcNow.AddSeconds(5);
var remaining = deadline - DateTimeOffset.UtcNow;
using var cts = new CancellationTokenSource(remaining);
Deadlines are useful when a request passes through multiple services. Each service should use the remaining budget instead of starting a fresh full timeout.
Common Timeout Values
There is no single correct timeout value. Timeout values should be based on:
- user experience requirements
- service-level objectives
- dependency latency distribution
- retry policy
- network environment
- endpoint purpose
- system capacity
- failure mode
Common production habits:
- Use short timeouts for interactive API calls.
- Use longer timeouts for reports, file processing, and batch jobs.
- Avoid infinite timeouts unless there is a clear reason.
- Make timeout values configurable.
- Monitor timeout rates and latency percentiles.
- Tune using real metrics, not guesses.
Testing Timeout Logic
Timeout logic should be tested without making unit tests slow or flaky.
Avoid this kind of test:
await Task.Delay(TimeSpan.FromSeconds(30));
Prefer small durations, fake dependencies, or time abstraction.
Example with a fake slow dependency:
public sealed class SlowPaymentGateway : IPaymentGateway
{
public async Task ChargeAsync(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
}
}
Test:
[Fact]
public async Task ChargeAsync_Throws_WhenGatewayTimesOut()
{
var service = new PaymentService(new SlowPaymentGateway());
await Assert.ThrowsAsync<OperationCanceledException>(() =>
service.ChargeAsync(TimeSpan.FromMilliseconds(50), CancellationToken.None));
}
In newer code, APIs that support TimeProvider can make time-based tests more deterministic.
Best Practices
Good timeout design includes both code-level and architecture-level habits.
Use cancellation-aware APIs:
await _dbContext.SaveChangesAsync(cancellationToken);
await _httpClient.SendAsync(request, cancellationToken);
await Task.Delay(delay, cancellationToken);
Propagate tokens through layers:
public Task<Order> GetOrderAsync(int id, CancellationToken cancellationToken)
{
return _repository.GetOrderAsync(id, cancellationToken);
}
Use local timeout only where ownership is clear:
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
callerToken,
timeoutCts.Token);
Handle timeout separately from caller cancellation:
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
// Local timeout.
}
catch (OperationCanceledException) when (callerToken.IsCancellationRequested)
{
// Caller canceled.
throw;
}
Log timeout context:
_logger.LogWarning(
"Order API timed out after {TimeoutSeconds} seconds for OrderId {OrderId}",
timeout.TotalSeconds,
orderId);
Common Mistakes
A common mistake is using Thread.Sleep in async code.
Thread.Sleep(5000); // Blocks a thread.
Use:
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
Another mistake is blocking on async work:
var result = GetDataAsync().Result;
This can cause deadlocks in some environments and wastes threads. Use:
var result = await GetDataAsync();
Another mistake is catching all exceptions and hiding timeout information:
catch (Exception)
{
return null;
}
This makes production debugging difficult. Log the timeout and preserve useful exception context.
Another mistake is retrying timeouts without a total budget. This can turn a small outage into a large traffic spike.
Another mistake is setting all timeouts to the same value. A search endpoint, a payment request, a report export, and a file upload usually need different timeout policies.