In this article, we’ll explore what the request timeouts middleware in ASP.NET Core does, how we can use it, as well as some advanced features we can leverage.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

Timeouts Middleware in ASP.NET Core

Applications using ASP.NET Core don’t apply global timeouts to HTTP requests by default. This is due to the simple reason that, as we all know, different endpoint requests can take drastically different times to return a result depending on various scenarios. Nevertheless, ASP.NET Core does provide us with the option to configure request timeouts via a middleware in .NET 8.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Moreover, using this middleware, we can configure global timeouts as well as specific endpoint timeouts. What’s even better is that we can use timeouts with minimal APIs, controllers, MVC, and even Razor Pages.

Specifically, when a timeout limit kicks in, the middleware updates the HttpContext.RequestAborted, by setting IsCancellationRequested to true. The requests are not automatically aborted; therefore, we can still return a result if the application doesn’t act on the IsCancellationRequested. The default behavior of  ASP.NET Core is to return status code 504.

Configuring the Timeout Middleware

Before we can configure the middleware, we need to create a simple Web API project. Specifically, our API will deal with Star Wars characters and return them upon our request.

Next, we create a simple Character class:

public class Character
{
    public required string Name { get; set; }
    public required int Height { get; set; }
    public required string BirthYear { get; set; }
    public required string Gender { get; set; }
}

In the class, we add four properties that will hold information about a particular character from the movies.

Following this, we add a service class:

public class StarWarsCharacterService : ICharacterService
{
    public async Task<Character> GetCharacterAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(2000, cancellationToken);

        return new()
        {
            Name = "Luke Skywalker",
            Height = 172,
            BirthYear = "19BBY",
            Gender = "Male"
        }; ;
    }
}

In detail, the StarWarsCharacterService has one method that uses the Task.Delay() method to postpone the execution for two seconds and then return the details for Luke Skywalker.

Adding the Request Timeouts Middleware

Now, let’s add the middleware:

var builder = WebApplication.CreateBuilder(args); 

builder.Services.AddControllers(); 
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddRequestTimeouts(); 
builder.Services.AddSwaggerGen();
builder.Services.AddTransient<ICharacterService, StarWarsCharacterService>(); 

var app = builder.Build(); 

if (app.Environment.IsDevelopment()) 
{ 
    app.UseSwagger(); 
    app.UseSwaggerUI(); 
} 

app.UseHttpsRedirection();
app.UseRequestTimeouts();
app.UseAuthorization();
app.MapControllers();

app.Run();

In the implementation of our Program.cs file, we start by calling the AddRequestTimeouts() method on our service collection. Subsequently, after this is done, we use the UseRequestTimeouts() method to add the middleware to the pipeline. 

Interestingly, those two lines are all we need to add the request timeouts middleware to our application. Moreover, there are several important things we must note here.

The first one is that timeouts are not configured by default, we must configure them. The second one is that if we are using the routing middleware, we must place our UseRequestTimeouts() method call after the UseRouting() method call. Finally, if we want to run and test this locally, we need to detach the debug process otherwise the timeouts won’t apply.

Configuring the Request Timeouts Middleware for Minimal APIs

Additionally, for minimal APIs, we have two options to configure the timeout – using either the attribute or the extension method.

Let’s start with the attribute:

app.MapGet("/GetCharacter",
    [RequestTimeout(milliseconds: 1000)] async (HttpContext context, ICharacterService characterService) =>
{
    return await characterService.GetCharacterAsync(context.RequestAborted);
});

We create the GetCharacter endpoint. It takes in an HttpContext and a ICharacterService. Inside the endpoint, we call the GetCharacterAsync() method of our service and we pass context.RequestAborted property which is a CancellationToken. We also decorate the endpoint with the RequestTimeout attribute and set the timeout to 1 second. 

Alternatively, we can use the WithRequestTimeout() extension method:

app.MapGet("/GetCharacter",
    async (HttpContext context, ICharacterService characterService) =>
{
    return await characterService.GetCharacterAsync(context.RequestAborted);
})
.WithRequestTimeout(TimeSpan.FromMilliseconds(1000));

We remove the attribute from our endpoint and add the WithRequestTimeout() method at the end. We also pass a TimeSpan representing the duration after which the request should be timed out.

Configuring the Request Timeouts Middleware for Controllers

It’s very easy to use the middleware with regular controllers as well:

[ApiController]
[Route("[controller]")]
public class StarWarsController : ControllerBase
{
    private readonly ICharacterService _characterService;

    public StarWarsController(ICharacterService characterService)
    {
        _characterService = characterService;
    }

    [HttpGet("GetCharacter")]
    [RequestTimeout(milliseconds: 1000)]
    public async Task<Character> GetCharacterAsync()
        => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);
}

Here, we create a controller and inject the service. Then, we create the GetCharacter endpoint which calls the service and again passes the RequestAborted property. With controller endpoints, the only way to use timeouts is by attributes, so we add the RequestTimeout attribute to our endpoint.

If we want the same request timeout for all of the endpoints within the controller, we can decorate the controller itself with the attribute.

Advanced Configuration Options

The request timeouts middleware provides us with ample options to fine-tune the conditions under which requests will time out.

Now, let’s take a look at some of them:

Configuring Multiple Endpoints with Named Policies

We can create named policies:

builder.Services.AddRequestTimeouts(options =>
{
    options.AddPolicy("OneSecondTimeout", TimeSpan.FromMilliseconds(1000));
});

Inside the AddRequestTimeouts() method in our Program class, we use the AddPolicy() method to create a named policy. We start by passing in the name followed by the delay we want. After this is done we can start applying the policy to the endpoints we desire by either passing the name of the policy to the RequestTimeout attribute or the WithRequestTimeout() method.

Applying this policy is easy:

app.MapGet("/GetCharacter",
    [RequestTimeout("OneSecondTimeout")] async (HttpContext context, ICharacterService characterService) =>
{
    return await characterService.GetCharacterAsync(context.RequestAborted);
});

app.MapGet("/GetCharacter",
    async (HttpContext context, ICharacterService characterService) =>
{
    return await characterService.GetCharacterAsync(context.RequestAborted);
})
.WithRequestTimeout("OneSecondTimeout");

For minimal APIs, we can use the two, already familiar approaches. We can either pass the name of our named policy to the RequestTimeout attribute or to the WithRequestTimeout() extension method.

The controller endpoint looks similar to the first example of the minimal API:

[HttpGet("GetCharacter")]
[RequestTimeout("OneSecondTimeout")]
public async Task<Character> GetCharacterAsync()
    => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);

We use the RequestTimeout attribute to apply the named policy to the GetCharacter endpoint. That’s it!

Setting Global Default Request Timeouts Policy

If needed, we can set up a global timeout:

builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromMilliseconds(1500)
    };
    options.AddPolicy("OneSecondTimeout", TimeSpan.FromMilliseconds(1000));
});

Consequently we achieve this by setting the DefaultPolicy property to a RequestTimeoutPolicy object we create. The global timeout will apply to all endpoints that don’t have a custom timeout defined.

Let’s see how it works:

[HttpGet("GetCharacter")]
[RequestTimeout("OneSecondTimeout")]
public async Task<Character> GetCharacterAsync()
    => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);

[HttpGet("GetCharacterWithDefaultTimeout")]
public async Task<Character> GetCharacterWithDefaultTimeoutAsync()
    => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);

Subsequently, within our StarWarsController, we add the GetCharacterWithDefaultTimeout endpoint. As we have configured a global timeout policy there is no need to do anything else.

In this example, calls to both endpoints will be timed out. A call to the GetCharacter endpoint will time out after one second due to the named policy and to the GetCharacterWithDefaultTimeout endpoint – after one and a half seconds due to the global policy. The same scenario can be applied to minimal APIs as well.

Setting Status Code in Request Timeouts Policy

Furthermore, the middleware also allows us to set a specific default status code:

builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromMilliseconds(1500),
        TimeoutStatusCode = (int)HttpStatusCode.InternalServerError
    };
    options.AddPolicy("OneSecondTimeout", TimeSpan.FromMilliseconds(1000));
});

To set the global status code we set the TimeoutStatusCode property of the RequestTimeoutPolicy instance to HttpStatusCode.InternalServerError. As a result, by doing this, every request that times out will have a status code of 500 (Internal Server Error).

Disabling Request Timeouts for Specific Endpoints

There are often special cases or endpoints that we would like to exclude from our default timeout policies. Thankfully the request timeouts middleware provides a means for disabling the timeout on specific endpoints.

For minimal APIs we can disable the timeouts using the DisableRequestTimeout attribute:

app.MapGet("/GetCharacter",
    [DisableRequestTimeout] async (HttpContext context, ICharacterService characterService) =>
{
    return await characterService.GetCharacterAsync(context.RequestAborted);
})

We can also disable timeouts via the DisableRequestTimeout() extension method:

app.MapGet("/GetCharacter",
    async (HttpContext context, ICharacterService characterService) =>
{
    return await characterService.GetCharacterAsync(context.RequestAborted);
})
.DisableRequestTimeout();

For controllers, we must use the DisableRequestTimeout attribute to override the default timeout policy:

[HttpGet("GetCharacter")]
[DisableRequestTimeout]
public async Task<Character> GetCharacterAsync()
    => await _characterService.GetCharacterAsync(HttpContext.RequestAborted);

Conclusion

The request timeouts middleware is a great addition to .NET 8, which helps in ensuring applications remain robust and responsive in the face of varying processing times. Its configuration is seamless, be it for Minimal APIs or controllers. Advanced options, from named policies to global defaults, empower us to have full control over request timeouts in our application by tailoring a combination of global and custom policies. 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!