Skip to content

Request Timeout Middleware in ConnectSoft Microservice Template

Purpose & Overview

Request Timeout Middleware enforces maximum time limits on HTTP requests, helping prevent resource exhaustion and ensuring that long-running requests are terminated appropriately. In the ConnectSoft Microservice Template, request timeout is implemented using ASP.NET Core's built-in request timeout middleware, providing configurable timeout policies for different endpoints and scenarios.

Request timeout provides:

  • Resource Protection: Prevents long-running requests from consuming server resources indefinitely
  • Predictable Behavior: Ensures requests complete within expected timeframes
  • Graceful Degradation: Allows services to handle timeouts gracefully with custom responses
  • Performance Monitoring: Helps identify slow endpoints that may need optimization
  • DoS Mitigation: Protects against slowloris attacks and resource exhaustion
  • User Experience: Provides timely feedback when operations take too long

Request Timeout Philosophy

Request timeout is a critical performance and reliability feature that should be configured based on your application's requirements. The template provides a global default timeout policy with the ability to define named policies for specific endpoints. Timeouts should be set based on actual endpoint performance characteristics and user expectations. Note that request timeouts do not trigger in debug mode.

Architecture Overview

Request Timeout in the Request Pipeline

Incoming Request
Routing Middleware (UseRouting)
Request Timeout Middleware (UseRequestTimeouts)
    ├── Check Timeout Policy
    ├── Start Timeout Timer
    ├── Monitor Request Duration
    │   ├── Within Timeout → Continue to endpoint
    │   └── Exceeds Timeout → Cancel request, return timeout response
Controller/Endpoint
Response

Request Timeout Components

RequestTimeoutExtensions.cs
├── AddMicroserviceRequestTimeouts() - Service Registration
│   ├── Default Policy (if Enabled)
│   │   ├── Timeout Duration
│   │   ├── Timeout Status Code
│   │   └── Custom Response Handler (optional)
│   └── Named Policies
│       ├── Policy Name
│       ├── Timeout Duration
│       ├── Timeout Status Code
│       └── Custom Response Handler (optional)
└── UseMicroserviceRequestTimeouts() - Middleware
    └── Place after UseRouting()

RequestTimeoutOptions.cs
├── Enabled (bool)
├── DefaultPolicy (RequestTimeoutPolicyOptions)
│   ├── Timeout (TimeSpan)
│   ├── TimeoutStatusCode (int)
│   └── WriteTimeoutResponse (Func<HttpContext, Task>)
└── Policies (Dictionary<string, RequestTimeoutPolicyOptions>)

Service Registration

AddMicroserviceRequestTimeouts Extension

Request timeout is registered via AddMicroserviceRequestTimeouts():

// MicroserviceRegistrationExtensions.cs
services.AddMicroserviceRequestTimeouts();

Implementation:

// RequestTimeoutExtensions.cs
internal static IServiceCollection AddMicroserviceRequestTimeouts(this IServiceCollection services)
{
    ArgumentNullException.ThrowIfNull(services);

    var options = OptionsExtensions.RequestTimeoutOptions;

    // Only register if enabled
    if (options?.Enabled == true)
    {
        services.AddRequestTimeouts(requestTimeoutOptions =>
        {
            // Configure default policy if provided
            if (options.DefaultPolicy != null)
            {
                requestTimeoutOptions.DefaultPolicy = new RequestTimeoutPolicy
                {
                    Timeout = options.DefaultPolicy.Timeout,
                    TimeoutStatusCode = options.DefaultPolicy.TimeoutStatusCode,
                    WriteTimeoutResponse = options.DefaultPolicy.WriteTimeoutResponse
                };
            }

            // Configure named policies
            if (options.Policies != null)
            {
                foreach (var policy in options.Policies)
                {
                    requestTimeoutOptions.AddPolicy(policy.Key, new RequestTimeoutPolicy
                    {
                        Timeout = policy.Value.Timeout,
                        TimeoutStatusCode = policy.Value.TimeoutStatusCode,
                        WriteTimeoutResponse = policy.Value.WriteTimeoutResponse
                    });
                }
            }
        });
    }

    return services;
}

UseMicroserviceRequestTimeouts Middleware

Pipeline Position:

// MicroserviceRegistrationExtensions.cs
application.UseMicroserviceRequestTimeouts();

Placement:

// Middleware order:
application.UseHttpsRedirection();
application.UseRouting();                      // Before request timeout
application.UseMicroserviceRequestTimeouts();  // After routing
application.UseMicroserviceRateLimiter();      // After request timeout
application.UseEndpoints(...);                 // After request timeout

Important: Request timeout middleware must be placed: - After UseRouting() (to access route information and apply named policies) - Before UseEndpoints() (to intercept requests before endpoint execution)

Configuration

RequestTimeoutOptions

Configuration Class:

// RequestTimeoutOptions.cs
public sealed class RequestTimeoutOptions
{
    public const string RequestTimeoutOptionsSectionName = "RequestTimeout";

    [Required]
    required public bool Enabled { get; set; } = true;

    [RequiredIf(nameof(Enabled), true)]
    [ValidateObjectMembers]
    public RequestTimeoutPolicyOptions? DefaultPolicy { get; set; }

    [ValidateObjectMembers]
    public Dictionary<string, RequestTimeoutPolicyOptions>? Policies { get; set; }
}

RequestTimeoutPolicyOptions

Configuration Class:

// RequestTimeoutPolicyOptions.cs
public sealed class RequestTimeoutPolicyOptions
{
    [Required]
    [Range(typeof(TimeSpan), "00:00:01", "1.00:00:00", ErrorMessage = "Timeout must be between 1 second and 1 hour.")]
    required public TimeSpan Timeout { get; set; }

    [Range(400, 599, ErrorMessage = "TimeoutStatusCode must be between 400 and 599.")]
    public int TimeoutStatusCode { get; set; } = 504;

    public Func<HttpContext, Task>? WriteTimeoutResponse { get; set; }
}

appsettings.json Configuration

Example Configuration:

{
  "RequestTimeout": {
    "Enabled": true,
    "DefaultPolicy": {
      "Timeout": "00:00:30",
      "TimeoutStatusCode": 504
    },
    "Policies": {
      "Fast": {
        "Timeout": "00:00:05",
        "TimeoutStatusCode": 504
      },
      "LongRunning": {
        "Timeout": "00:02:00",
        "TimeoutStatusCode": 504
      },
      "FileUpload": {
        "Timeout": "00:05:00",
        "TimeoutStatusCode": 504
      }
    }
  }
}

Configuration Parameters:

Parameter Type Description Default Example
Enabled bool Enable or disable request timeout middleware true true
DefaultPolicy RequestTimeoutPolicyOptions Default timeout policy (required when Enabled is true) Required See below
Policies Dictionary<string, RequestTimeoutPolicyOptions> Named timeout policies for specific endpoints Optional See below

RequestTimeoutPolicyOptions Parameters:

Parameter Type Description Default Example
Timeout TimeSpan Maximum request duration (1 second to 1 hour) Required 00:00:30 (30 seconds)
TimeoutStatusCode int HTTP status code to return on timeout (400-599) 504 504 (Gateway Timeout)
WriteTimeoutResponse Func<HttpContext, Task> Custom response handler (not configurable via appsettings) null N/A

Environment-Specific Configuration

Development:

{
  "RequestTimeout": {
    "Enabled": true,
    "DefaultPolicy": {
      "Timeout": "00:01:00",
      "TimeoutStatusCode": 504
    },
    "Policies": {
      "Fast": {
        "Timeout": "00:00:10",
        "TimeoutStatusCode": 504
      },
      "LongRunning": {
        "Timeout": "00:05:00",
        "TimeoutStatusCode": 504
      }
    }
  }
}

Production:

{
  "RequestTimeout": {
    "Enabled": true,
    "DefaultPolicy": {
      "Timeout": "00:00:30",
      "TimeoutStatusCode": 504
    },
    "Policies": {
      "Fast": {
        "Timeout": "00:00:05",
        "TimeoutStatusCode": 504
      },
      "LongRunning": {
        "Timeout": "00:02:00",
        "TimeoutStatusCode": 504
      }
    }
  }
}

Testing:

{
  "RequestTimeout": {
    "Enabled": false
  }
}

Applying Timeout Policies

Default Policy

The default policy applies to all endpoints that don't have an explicit timeout policy configured:

// All endpoints use the default policy automatically
[HttpGet("api/products")]
public async Task<IActionResult> GetProducts()
{
    // Uses default timeout policy
}

Named Policies

Apply named policies to specific endpoints using the [RequestTimeout] attribute:

[RequestTimeout("Fast")]
[HttpGet("api/health")]
public async Task<IActionResult> HealthCheck()
{
    // Uses "Fast" policy (5 seconds)
}

[RequestTimeout("LongRunning")]
[HttpPost("api/reports/generate")]
public async Task<IActionResult> GenerateReport()
{
    // Uses "LongRunning" policy (2 minutes)
}

Per-Endpoint Timeout

You can also specify a timeout directly on an endpoint:

[RequestTimeout(5000)] // 5 seconds
[HttpGet("api/search")]
public async Task<IActionResult> Search()
{
    // Uses 5-second timeout
}

Disabling Timeout for Specific Endpoints

Disable timeout for endpoints that should not be subject to timeout limits:

[DisableRequestTimeout]
[HttpPost("api/long-running-operation")]
public async Task<IActionResult> LongRunningOperation()
{
    // No timeout applied
}

Custom Timeout Responses

Default Response

By default, when a timeout occurs, the middleware returns the configured TimeoutStatusCode (default: 504 Gateway Timeout) with an empty response body.

Custom Response Handler

You can provide a custom response handler programmatically:

// RequestTimeoutExtensions.cs
services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(30),
        TimeoutStatusCode = 504,
        WriteTimeoutResponse = async context =>
        {
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsJsonAsync(new
            {
                error = "Request timeout",
                message = "The request took too long to complete.",
                timeout = "30 seconds"
            });
        }
    };
});

Note: Custom response handlers cannot be configured via appsettings.json and must be set programmatically.

Response Behavior

Timeout Response

When a request exceeds the timeout:

HTTP/1.1 504 Gateway Timeout
Content-Type: text/plain; charset=utf-8
Content-Length: 0

Or with custom response handler:

HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
Content-Length: 85

{
  "error": "Request timeout",
  "message": "The request took too long to complete.",
  "timeout": "30 seconds"
}

Request Cancellation

When a timeout occurs: - The request's CancellationToken is canceled - The endpoint should handle cancellation gracefully - Any ongoing operations should be aborted - Resources should be cleaned up

Example:

[HttpGet("api/data")]
public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
    try
    {
        var data = await _service.GetDataAsync(cancellationToken);
        return Ok(data);
    }
    catch (OperationCanceledException)
    {
        // Request was canceled (likely due to timeout)
        return StatusCode(504, "Request timeout");
    }
}

Debug Mode Behavior

Debug Mode Limitation

Request timeouts do not trigger in debug mode. This is by design to allow developers to debug long-running operations without interruption. To test timeout behavior, run the application in Release mode or use integration tests.

Testing Timeouts:

// RequestTimeoutMiddlewareTests.cs
[TestMethod]
public async Task RequestShouldTimeoutAfterConfiguredDuration()
{
    // Note: This test may not work in debug mode
    // Run tests in Release mode or use integration tests
}

Best Practices

Do's

  1. Enable Request Timeout in Production

    {
      "RequestTimeout": {
        "Enabled": true,
        "DefaultPolicy": {
          "Timeout": "00:00:30"
        }
      }
    }
    

  2. Set Appropriate Timeouts

    // ✅ GOOD - Based on endpoint characteristics
    {
      "DefaultPolicy": { "Timeout": "00:00:30" },
      "Policies": {
        "Fast": { "Timeout": "00:00:05" },
        "LongRunning": { "Timeout": "00:02:00" }
      }
    }
    

  3. Use Named Policies for Different Endpoint Types

    // ✅ GOOD - Different timeouts for different operations
    [RequestTimeout("Fast")]
    [HttpGet("api/health")]
    
    [RequestTimeout("LongRunning")]
    [HttpPost("api/reports")]
    

  4. Handle Cancellation Gracefully

    // ✅ GOOD - Handle cancellation
    public async Task<IActionResult> GetData(CancellationToken cancellationToken)
    {
        try
        {
            var data = await _service.GetDataAsync(cancellationToken);
            return Ok(data);
        }
        catch (OperationCanceledException)
        {
            return StatusCode(504);
        }
    }
    

  5. Monitor Timeout Metrics

  6. Track timeout occurrences
  7. Monitor endpoint response times
  8. Alert on high timeout rates
  9. Identify slow endpoints

  10. Disable Timeout for Long-Running Operations

    // ✅ GOOD - Disable for operations that legitimately take a long time
    [DisableRequestTimeout]
    [HttpPost("api/batch-processing")]
    

Don'ts

  1. Don't Disable Timeout in Production

    // ❌ BAD - No protection against resource exhaustion
    {
      "RequestTimeout": {
        "Enabled": false
      }
    }
    

  2. Don't Set Timeouts Too Short

    // ❌ BAD - Will timeout legitimate requests
    {
      "DefaultPolicy": {
        "Timeout": "00:00:01"  // Too short for most operations
      }
    }
    

  3. Don't Set Timeouts Too Long

    // ❌ BAD - Defeats the purpose of timeout protection
    {
      "DefaultPolicy": {
        "Timeout": "1.00:00:00"  // 24 hours - too long
      }
    }
    

  4. Don't Ignore Cancellation Tokens

    // ❌ BAD - Doesn't respect cancellation
    public async Task<IActionResult> GetData()
    {
        var data = await _service.GetDataAsync(); // No cancellation token
        return Ok(data);
    }
    

  5. Don't Forget to Test Timeout Behavior

    // ❌ BAD - Timeout behavior not tested
    // Deploy without testing timeout scenarios
    
    // ✅ GOOD - Test timeout behavior
    // Run integration tests in Release mode
    

Advanced Scenarios

Custom Timeout Response

Programmatic Configuration:

services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(30),
        TimeoutStatusCode = 504,
        WriteTimeoutResponse = async context =>
        {
            context.Response.ContentType = "application/json";
            var response = new
            {
                error = "Request timeout",
                message = "The request exceeded the maximum allowed time.",
                timeout = "30 seconds",
                requestId = context.TraceIdentifier
            };
            await context.Response.WriteAsJsonAsync(response);
        }
    };
});

Conditional Timeout Policies

Based on Request Characteristics:

services.AddRequestTimeouts(options =>
{
    // Default policy
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(30)
    };

    // Fast policy for health checks
    options.AddPolicy("Fast", new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(5)
    });

    // Long-running policy for reports
    options.AddPolicy("LongRunning", new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromMinutes(5)
    });
});

Timeout with Retry Logic

Combining Timeout with Retry:

[RequestTimeout("Fast")]
[HttpGet("api/data")]
public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
    var retryPolicy = Policy
        .Handle<HttpRequestException>()
        .Or<OperationCanceledException>()
        .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

    try
    {
        var result = await retryPolicy.ExecuteAsync(async ct =>
        {
            return await _httpClient.GetAsync("https://api.example.com/data", ct);
        }, cancellationToken);

        return Ok(await result.Content.ReadAsStringAsync());
    }
    catch (OperationCanceledException)
    {
        return StatusCode(504, "Request timeout after retries");
    }
}

Troubleshooting

Issue: Timeouts Not Triggering

Symptoms: Requests exceeding timeout duration are not being canceled.

Solutions: 1. Verify Timeout is Enabled

{
  "RequestTimeout": {
    "Enabled": true
  }
}

  1. Check Middleware Order

    // ✅ GOOD - Correct order
    application.UseRouting();
    application.UseMicroserviceRequestTimeouts();
    application.UseEndpoints(...);
    

  2. Run in Release Mode

  3. Timeouts do not trigger in debug mode
  4. Run application in Release mode to test timeout behavior
  5. Use integration tests to verify timeout functionality

  6. Verify Configuration is Loaded

  7. Check RequestTimeoutOptions is registered
  8. Verify appsettings.json contains request timeout section
  9. Check options validation passes

Issue: Too Many Timeouts

Symptoms: Legitimate requests are timing out.

Solutions: 1. Increase Timeout Duration

{
  "DefaultPolicy": {
    "Timeout": "00:01:00"  // Increase from 30 seconds
  }
}

  1. Use Named Policies for Slow Endpoints

    [RequestTimeout("LongRunning")]
    [HttpPost("api/slow-operation")]
    

  2. Disable Timeout for Specific Endpoints

    [DisableRequestTimeout]
    [HttpPost("api/legitimate-long-operation")]
    

  3. Optimize Slow Endpoints

  4. Review endpoint performance
  5. Identify bottlenecks
  6. Optimize database queries
  7. Add caching where appropriate

Issue: Timeout Response Not Customized

Symptoms: Default timeout response is returned instead of custom response.

Solutions: 1. Verify Custom Response Handler is Configured

// Custom response handlers must be set programmatically
options.DefaultPolicy.WriteTimeoutResponse = async context =>
{
    // Custom response logic
};

  1. Check Response Handler is Not Null
  2. Ensure WriteTimeoutResponse is set
  3. Verify handler is not being overridden

Issue: Cancellation Not Handled

Symptoms: Endpoints don't respond to cancellation, resources not cleaned up.

Solutions: 1. Accept CancellationToken

public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
    // Use cancellationToken in async operations
}

  1. Handle OperationCanceledException

    try
    {
        await _service.ProcessAsync(cancellationToken);
    }
    catch (OperationCanceledException)
    {
        // Handle cancellation gracefully
    }
    

  2. Pass CancellationToken to All Async Operations

    await _database.QueryAsync(query, cancellationToken);
    await _httpClient.GetAsync(url, cancellationToken);
    await _cache.GetAsync(key, cancellationToken);
    

Summary

Request timeout middleware in the ConnectSoft Microservice Template provides:

  • Global Default Timeout: Configurable default timeout policy
  • Named Policies: Different timeout policies for different endpoints
  • Per-Endpoint Timeouts: Override timeout for specific endpoints
  • Custom Responses: Programmatic custom timeout response handlers
  • Graceful Cancellation: Automatic request cancellation on timeout
  • Configurable Status Codes: Custom HTTP status codes for timeouts
  • Production Ready: Configurable for different environments

By following these patterns, teams can:

  • Protect Resources: Prevent long-running requests from consuming resources indefinitely
  • Ensure Predictability: Guarantee requests complete within expected timeframes
  • Improve User Experience: Provide timely feedback when operations take too long
  • Monitor Performance: Identify slow endpoints that need optimization
  • Handle Timeouts Gracefully: Implement proper cancellation handling

Request timeout is an essential performance and reliability feature that protects microservices from resource exhaustion while ensuring predictable request behavior.