DEV_NET_CORE
GET_STARTED
.NETAsync programming, tasks, cancellation, and concurrency

Timeouts in C#

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:

  • CancellationTokenSource
  • CancellationToken
  • CancelAfter
  • Task.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:

Code
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:

Code
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:

Code
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:

Code
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:

Code
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 CancellationToken in async methods that may take time.
  • Pass the token to every async operation that supports it.
  • Use ThrowIfCancellationRequested in 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.

Code
using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(3));

await DownloadFileAsync(url, cts.Token);

A shorter form is:

Code
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:

Code
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.

Code
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.

Code
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:

Code
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.

Code
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:

Code
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

var result = await LoadDataAsync(cts.Token);

If you use Task.Delay, pass a token when appropriate:

Code
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.

Code
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:

Code
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:

Code
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 HttpClient for every request.
  • Prefer IHttpClientFactory in 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.ConnectTimeout when connection establishment needs a separate limit.

Example with connection timeout:

Code
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:

  • OperationCanceledException
  • TaskCanceledException
  • TimeoutException
  • 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:

Code
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:

Code
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:

Code
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:

Code
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:

Code
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.

Code
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:

Code
// 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:

Code
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:

Code
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

Deadline example:

Code
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:

Code
await Task.Delay(TimeSpan.FromSeconds(30));

Prefer small durations, fake dependencies, or time abstraction.

Example with a fake slow dependency:

Code
public sealed class SlowPaymentGateway : IPaymentGateway
{
    public async Task ChargeAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
    }
}

Test:

Code
[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:

Code
await _dbContext.SaveChangesAsync(cancellationToken);
await _httpClient.SendAsync(request, cancellationToken);
await Task.Delay(delay, cancellationToken);

Propagate tokens through layers:

Code
public Task<Order> GetOrderAsync(int id, CancellationToken cancellationToken)
{
    return _repository.GetOrderAsync(id, cancellationToken);
}

Use local timeout only where ownership is clear:

Code
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    callerToken,
    timeoutCts.Token);

Handle timeout separately from caller cancellation:

Code
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
    // Local timeout.
}
catch (OperationCanceledException) when (callerToken.IsCancellationRequested)
{
    // Caller canceled.
    throw;
}

Log timeout context:

Code
_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.

Code
Thread.Sleep(5000); // Blocks a thread.

Use:

Code
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);

Another mistake is blocking on async work:

Code
var result = GetDataAsync().Result;

This can cause deadlocks in some environments and wastes threads. Use:

Code
var result = await GetDataAsync();

Another mistake is catching all exceptions and hiding timeout information:

Code
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.

Interview Practice

PreviousException Handling in C#Next UpConfiguration Sources and the Options Pattern