Increase UI Performance using Timeout Middleware in .NET Core

Hello friends, today I came up with a new article in .NET core, Timeout middleware; let’s learn about it and see where we can apply it in real-time.

Real-Time Use Case

In a real-time application, such as a financial trading platform, timely responses are crucial. If a service that provides stock prices or executes trades takes too long to respond, it could lead to significant financial losses. Timeout middleware can be used to ensure that if these services do not respond within a specified time frame, the request is aborted, and an error is returned to the user.

Understanding Timeout Middleware

ASP.NET Core servers don’t do this by default since request processing times vary widely by scenario. For example, WebSockets, static files, and calling expensive APIs would each require a different timeout limit. So ASP.NET Core provides middleware that configures timeouts per endpoint as well as a global timeout.

Benefits of Timeout Middleware

  1. Improved Reliability: Ensures that your application does not hang indefinitely due to slow external dependencies or internal processing, improving overall reliability.
  2. Resource Management: Prevents resources from being tied up by long-running requests, freeing them up for other requests.
  3. User Experience: Provides a better user experience by returning a timely response, even if it’s an error, rather than making users wait indefinitely.
  4. Error Handling: Allows for a centralized way to handle request timeouts, making it easier to log and manage these occurrences.
  5. Security: Reduces the attack surface for denial-of-service (DoS) attacks by limiting the time any request can consume server resources.

Learn more about the DoS attack in the below article.

When to Use Timeout Middleware?

Timeout middleware is particularly useful in the following scenarios.

  1. Preventing Resource Exhaustion: Ensuring that long-running requests do not consume server resources indefinitely.
  2. Improving User Experience: Providing timely feedback to users when a request cannot be completed within a reasonable timeframe.
  3. Maintaining Application Responsiveness: Keeping the application responsive by terminating requests that exceed a certain duration.
  4. Enforcing SLAs (Service Level Agreements): Ensuring that the application meets predefined performance and response time criteria.
  5. Handling Unpredictable Load: Managing request time during periods of high traffic or when dealing with unpredictable workloads.

Implementation

There are multiple ways to implement it.

  • We can create Custom Middleware to configure Timeout
  • We can use the [RequestTimeout] attribute at the Controller and Action Level.

1. Custom Timeout Middleware

Consider the code below.

public class TimeoutClass
{
    private readonly RequestDelegate _next;
    private readonly TimeSpan _timeout;
    public TimeoutClass(RequestDelegate next, TimeSpan timeout)
    {
        _next = next;
        _timeout = timeout;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        using (var cts = new CancellationTokenSource(_timeout))
        {
            try
            {
                context.RequestAborted = cts.Token;
                await _next(context);
            }
            catch (OperationCanceledException)
            {
                context.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
                await context.Response.WriteAsync("Request timed out happening.");
            }
        }
    }
}

We have created a Timeout class and injected RequestDelegate and timeout time via the constructor.

Inside the Invoke Method, we set the Cancellation Token with a specified timeout. When a timeout limit is hit, a CancellationToken in HttpContext.RequestAborted has IsCancellationRequested set to true. Abort() isn't automatically called on the request, so the application may still produce a success or failure response. The default behavior if the app doesn't handle the exception and produce a response is to return status code 504.

Registering TimeoutClass Middleware

public static class TimeoutMiddlewareExtensions
{
    public static IApplicationBuilder UseTimeoutMiddleware(this IApplicationBuilder builder, TimeSpan timeout)
    {
        return builder.UseMiddleware<TimeoutClass>(timeout);
    }
}

Program. cs

using Microsoft.AspNetCore.Http.Timeouts;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseTimeoutMiddleware(TimeSpan.FromSeconds(10));
app.UseAuthorization();
app.MapControllers();
app.Run();

Explanation

  • Timeoutclass: This middleware class sets a timeout for each request. If the request is not complete within the specified time (_timeout), an OperationCanceledException is thrown, and a 504 Gateway Timeout response is returned.
  • UseTimeoutMiddleware: This extension method allows you to add the timeout middleware to the middleware pipeline with a specified timeout.
  • Startup Configuration: The middleware is added to the pipeline in the Configure method with a timeout of 10 seconds. Adjust the timeout value based on your application's needs.

KeyPoint: we have set up Timeout =10, which means if any request takes more than 10 sec, a timeout exception will call.

app.UseTimeoutMiddleware(TimeSpan.FromSeconds(10));

Testing TimeoutClass Middleware: Create Below API EndPoint.

namespace TimeoutMiddleware.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DemoTimeOutConttoller : ControllerBase
    {
        [HttpGet("TestTimeOutMiddleware/{delay:int}")]
        public async Task<IActionResult> TestTimeOutMiddleware([FromRoute] int delay)
        {
            // in real time there will be long running http call or long running db call
            await Task.Delay(TimeSpan.FromSeconds(delay), HttpContext.RequestAborted);
            return Ok();
        }

    }
}

Here, we are passing delay time from user input. As per the Code, we are expecting that if the delay time is more than 10 sec, then we should get a timeout exception

Let’s Execute this code

Code

Localhost

we are getting a timeout exception as expected.

2. [RequestTimeout()] Attribute

we can implement request time out using the [RequestTimeout(“1000”)] attribute at the controller level or action level.

Steps

1. Update Program .cs with the below code.

Code

using Microsoft.AspNetCore.Http.Timeouts;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddRequestTimeouts();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// app.UseTimeoutMiddleware(TimeSpan.FromSeconds(10));
app.UseAuthorization();
app.UseRequestTimeouts();
app.MapControllers();

app.Run();

2. Apply at the controller Level.

using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[RequestTimeout("00:00:05")] // Timeout of 5 seconds
public class MyController : ControllerBase
{
    [HttpGet("TimeOutFunction")]
    public async Task<IActionResult> TimeOutFunction()
    {
        // Simulate a long-running task  like long running db call and http call
        await Task.Delay(10000); // 10 seconds
        return Ok("Operation completed.");
    }
}

Explanation: Using the below attribute means we have configured all endpoints in the controller level for timeout =5 sec, which means if any endpoints take more than 5 sec time out exception will call.

[RequestTimeout("00:00:05")] // Timeout of 5 seconds

3. At the Action Level.

[ApiController]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
    [HttpGet("TwoSecondTimeout")]
    [RequestTimeout("00:00:02")] // Timeout of 2 seconds
    public async Task<IActionResult> TwoSecondTimeout()
    {
        // Simulate a task that finishes quickly
        await Task.Delay(1000); // 1 second
        return Ok("Done with work.");
    }
    [HttpGet("FiveSecondTimeout")]
    [RequestTimeout("00:00:05")] // Timeout of 5 seconds
    public async Task<IActionResult> FiveSecondTimeout()
    {
        // Simulate a long-running task
        await Task.Delay(10000); // 10 seconds
        return Ok("work completed.");
    }
}

We have applied this attribute at the action level as if we configured each endpoint with its own timeout period.

3. Configure Timeout for Minimal API

For minimal API apps, configure an endpoint to timeout by calling WithRequestTimeout, or by applying the [RequestTimeout] attribute, as shown in the following example.

using Microsoft.AspNetCore.Http.Timeouts;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts();
var app = builder.Build();
app.UseRequestTimeouts();

//WAY 1
app.MapGet("/TwoSecondTimeout", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }
    return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout(TimeSpan.FromSeconds(2));
// Returns "Timeout!"
//WAY 2  with Attribure 
app.MapGet("/TwoSecondTimeout",
    [RequestTimeout(milliseconds: 2000)] async (HttpContext context) => {
        try
        {
            await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
        }
        catch (TaskCanceledException)
        {
            return Results.Content("Timeout!", "text/plain");
        }
        return Results.Content("No timeout!", "text/plain");
    });
// Returns "Timeout!"

app.Run();

4. Other Scenario’s

Suppose we have 10 endpoints in the application, and we want to configure 4 endpoints with a 5-second timeout and 6 Endpoints with a 15-second Timeout. Then, we can define policies as follows:

builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy = new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
    options.AddPolicy("TwoSecondPolicy", TimeSpan.FromSeconds(2));
});
app.MapGet("/namedpolicy", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout("TwoSecondPolicy");
// Returns "Timeout!"

Global Setup of timeout for all endpoints in the application.

// Adding timeout middleware
app.Use(async (context, next) =>
{
    var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    context.RequestAborted = cancellationTokenSource.Token;

    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
        context.Response.StatusCode = StatusCodes.Status408RequestTimeout;
    }
});

Conclusion

We have learned about Timeout middleware, its multiple implementation methods, and its importance from a business perspective.

Thanks!


Similar Articles