In this article, we’ll demonstrate how to set a default request timeout for all requests and configure specific request timeout settings for individual requests using HttpClient.

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

In modern software development, managing HTTP requests efficiently is vital for keeping our applications fast and reliable. The HttpClient enables us to effectively execute HTTP calls in .NET applications. Properly managing timeouts is essential to prevent delays caused by slow network responses.

Let’s start.

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

Understanding HttpClient Request Timeout

By default, HttpClient in .NET times out after 100 seconds, raising a TaskCanceledException if no response is received. Adjusting this timeout is key to enhancing performance in fast operations and extending wait times for longer processes. So, properly managing this setting improves user experience, application reliability, and responsiveness, ensuring efficiency under various network conditions.

If you want to find out more about the HttpClient, check out our article HttpClient with ASP.NET Core Tutorial.

Now, let’s move on to explore some practical scenarios.

Set a Default (Global) Request Timeout

The global timeout for an HttpClient instance affects all requests and is set through the Timeout property. Developers usually retrieve this value from the application’s configuration, making adjustments easier. Setting a consistent global timeout balances responsiveness with sufficient processing time for requests under different network conditions.

Let’s consider an example where we configure a global timeout in appsettings.json:

{
  "TestClient": {
    "TimeOutSeconds": 3
  }
}

Next, we apply this timeout to HttpClient:

builder.Services.AddHttpClient("TestClient", (sp, httpClient) =>
{
    var configuration = sp.GetRequiredService<IConfiguration>();
    var timeoutSeconds = configuration.GetValue<int>("TestClient:TimeOutSeconds");

    httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
});

We read the timeout setting from the configuration provided by the service provider. Consequently, this configuration ensures all requests via this configured HttpClient resolved with the name TestClient maintain a uniform timeout of 3 seconds.

Let’s look at a scenario where we send a request to an external endpoint: 

app.MapGet("/api/test-global-timeout", async (IHttpClientFactory httpClientFactory) =>
{
    var httpClient = httpClientFactory.CreateClient("TestClient");

    try
    {
        var response = await httpClient.GetAsync("/api/delay-4-seconds");

        return Results.Ok();
    }
    catch (TaskCanceledException)
    {
        return Results.Text("TaskCanceledException: HttpClient global timeout passed");
    }
});

Here, we have an endpoint that responds in 4 seconds—1 second more than our configured global timeout.

Consequently, the HttpClient throws TaskCanceledException and cancels the request, illustrating the importance of aligning timeouts with expected response times.

Set a Per-Request Timeout

Moreover, while a well-adjusted global request timeout maintains our application’s responsiveness, setting per-request timeouts for each HTTP call enhances this responsiveness.

We establish these timeouts by either creating a custom cancellation token with a specified timeout or, in web applications, using the CancellationToken from the controller’s action or, as in our example, minimal API. This token represents the HttpContext.Aborted token from the incoming user’s request, providing an effective way to manage timeouts at the request level. Moreover, this approach proves particularly beneficial as it immediately halts the process if a client cancels their request, thus conserving server resources and time.

Now, let’s create another endpoint to test the per-request timeout:

app.MapGet(
    "/api/test-per-request-timeout", 
    async (IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
    {
        var httpClient = httpClientFactory.CreateClient("TestClient");

        try
        {
            var response = await httpClient.GetAsync($"/api/delay-4-seconds", cancellationToken);

            return Results.Ok();
        }
        catch (TaskCanceledException)
        {
            return Results.Text("TaskCanceledException: User request cancelled");
        }
    }
);

In this case, we use the HttpContext.Aborted cancellationToken, which cancels the operation if the user aborts the request.

We can use this method in conjunction with the global timeout, where the shorter duration between the two determines when the request is canceled. This strategy not only enhances responsiveness by swiftly freeing up resources but also allows detailed control over the server’s wait time, tailored to the specific needs of each request.

Implement Additional Request Timeout

Sometimes, even with a global timeout and a Context.Aborted token, a particular request might need a dedicated timeout setting. This request-specific timeout offers more granular control over response times, independent of broader settings or client-side cancellations.

Let’s look at an example of how we can implement an additional combined timeout:

app.MapGet(
    "/api/test-combined-timeout", 
    async (IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
    {
        var httpClient = httpClientFactory.CreateClient("TestClient");

        using var endpointSpecificToken = new CancellationTokenSource(TimeSpan.FromSeconds(2));
        using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken, 
            endpointSpecificToken.Token
        );

        try
        {
            var response = await httpClient.GetAsync("/api/delay-4-seconds", tokenSource.Token);

            return Results.Ok();
        }
        catch (TaskCanceledException)
        {
            return endpointSpecificToken.IsCancellationRequested
                ? Results.Text("TaskCanceledException: Specific token canceled")
                : Results.Text("TaskCanceledException: HttpClient global timeout passed");
        }
    }
);

Here, we use a linked token source tokenSource combining the request’s cancellationToken with a new, stricter 2-second timeout endpointSpecificToken. The principle remains—the shortest of the three cancellation sources—global timeout, user’s cancellation token, and the specific token—will abort the operation.

Ultimately, this precise management of timeouts is tailored to the urgency of the request and it ensures efficient and timely operations.

Handling Timeouts Gracefully

In the examples we’ve covered, the catch blocks managing TaskCanceledException stand out. Catching this exception during timeouts enables our application to respond effectively. We can either retry the operation or inform the user.

A common strategy among developers is to implement retry mechanisms using libraries like Polly, which provides a straightforward and efficient way to establish complex retry strategies and fallback methods.

Conclusion

In this article, we have learned that effectively managing timeouts in HttpClient is crucial for building responsive and reliable .NET applications. We accomplish this by carefully configuring both global and per-request timeouts. Additionally, by prioritizing graceful handling of timeouts we improve user experience and maintain the application’s performance.

Now, let’s return to the applications we are developing and implement what we’ve learned today.

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