Long-running requests are rough on everyone. Users wait, reverse proxies retry, and thread pool pressure quietly ramps up.
ASP.NET Core's request timeout middleware gives you a clear upper bound so slow paths fail fast and predictably.
Enable Request Timeouts
builder.Services.AddRequestTimeouts(options =>
{
options.DefaultPolicy = new RequestTimeoutPolicy
{
Timeout = TimeSpan.FromSeconds(10),
TimeoutStatusCode = StatusCodes.Status503ServiceUnavailable
};
options.AddPolicy("long-running", TimeSpan.FromSeconds(30));
});
var app = builder.Build();
app.UseRequestTimeouts();
Now requests that exceed policy limits are cancelled and return the configured status code.
Apply Per Endpoint
app.MapGet("/reports/summary", async (IReportService reports, CancellationToken ct) =>
{
var model = await reports.BuildSummaryAsync(ct);
return Results.Ok(model);
});
app.MapPost("/reports/rebuild", async (IReportService reports, CancellationToken ct) =>
{
await reports.RebuildSnapshotsAsync(ct);
return Results.Accepted();
})
.WithRequestTimeout("long-running");
You can keep aggressive defaults and only relax limits where you intentionally expect longer work.
CancellationToken Matters
Timeout middleware is only effective if downstream code respects cancellation:
public async Task<SummaryDto> BuildSummaryAsync(CancellationToken ct)
{
var rows = await _db.Orders
.AsNoTracking()
.Where(x => x.CreatedAtUtc >= DateTime.UtcNow.AddDays(-30))
.ToListAsync(ct);
ct.ThrowIfCancellationRequested();
return Summarise(rows);
}
If services ignore the token, the app still burns resources after the client has already given up.
Wrapping Up
Request timeout middleware is a practical guardrail. It protects capacity, improves predictability, and gives clients a fast failure signal instead of hanging forever.
If you haven't set explicit request time budgets yet, this is an easy reliability improvement.
