Programming

C# HttpClient Deadlock: Fix Hanging API Calls

Resolve C# HttpClient deadlock issues causing hanging API calls. Learn proper async usage, ConfigureAwait, and IHttpClientFactory to prevent freezes.

1 answer 1 view

Why does my HttpClient call hang and not return a response in C#? My async method to call an external API never completes, causing my application to freeze. Here’s my code:

csharp
private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    string result;

    using (HttpClient client = new HttpClient())
    {
        HttpResponseMessage response = await client.GetAsync(url);

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
        }

        result = await response.Content.ReadAsStringAsync();
    }

    return result;
}

And I’m calling it like this:

csharp
Task<string[]> taskRes = GetLocation(ap.Latitude, ap.Longitude);
string[] loc = taskRes.Result;

What am I doing wrong and how can I fix this issue?

Your C# HttpClient call is hanging due to a classic async deadlock caused by blocking on the Result property of a task that needs to complete on the same thread context. Additionally, you’re creating a new HttpClient instance each time, which can lead to socket exhaustion and connection issues.

Contents

Understanding the HttpClient Deadlock Problem

When your HttpClient call hangs, it’s often due to a synchronization context deadlock. In your code, you’re using await client.GetAsync(url) in an async method, but then blocking with taskRes.Result in the calling code. This creates a classic deadlock scenario where the async method is waiting to resume on the original context, but that context is blocked waiting for the method to complete.

The official Microsoft documentation explains this issue clearly: when you have an async method that needs to complete on the original synchronization context, and you block that context with .Result or .Wait(), a deadlock occurs because the async method can’t resume execution on the blocked context.

In your specific case, the calling code is blocking the thread with taskRes.Result, which prevents the async method from completing its execution. This is why your application freezes and the HttpClient call never returns a response.

The Root Cause: Synchronization Context Deadlock

The deadlock happens because of how async/await works with synchronization contexts. When you use await in an async method, the method returns a Task without blocking the calling thread. When the awaited operation completes, the method resumes execution on the original synchronization context by default.

Here’s what happens in your code:

  1. GetLocation starts execution and calls await client.GetAsync(url)
  2. The method returns a Task<string[]> immediately without waiting
  3. The calling code blocks with taskRes.Result, waiting for the Task to complete
  4. The HTTP client completes its request and tries to resume execution of GetLocation on the original synchronization context
  5. But that context is blocked by taskRes.Result, creating a deadlock

As explained in Stephen Cleary’s authoritative guide on async code, “the default TaskFactory will try to marshal interrupt callbacks back to the SynchronizationContext, which is likely your UI thread or ASP.NET request context” - this is exactly what’s causing your deadlock.

Solution 1: Proper Async Usage

The simplest fix is to make your calling code fully async as well. Instead of blocking with .Result, you should use await:

csharp
// Instead of:
Task<string[]> taskRes = GetLocation(ap.Latitude, ap.Longitude);
string[] loc = taskRes.Result;

// Use:
string[] loc = await GetLocation(ap.Latitude, ap.Longitude);

This approach eliminates the deadlock because there’s no longer a thread being blocked while waiting for an async operation to complete. The calling method should also be marked as async:

csharp
private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = new HttpClient())
    {
        HttpResponseMessage response = await client.GetAsync(url);

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
        }

        string result = await response.Content.ReadAsStringAsync();
        return new string[] { result }; // Return as array if needed
    }
}

And the calling code should be:

csharp
// Make sure this method is also marked as async
private async Task ProcessLocation()
{
    try
    {
        string[] loc = await GetLocation(ap.Latitude, ap.Longitude);
        // Process the location data
    }
    catch (Exception ex)
    {
        // Handle exceptions
    }
}

This solution follows the fundamental rule of async programming: never block on async code. As stated in the Stack Overflow discussion, “You should ONLY ever use async void when coupled with an event handler” and blocking on async code is a common source of deadlocks.

Solution 2: ConfigureAwait(false) for Library Code

If you can’t change the calling code to be fully async (for example, if it’s in a library or framework code), you can use ConfigureAwait(false) to avoid capturing the synchronization context:

csharp
private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = new HttpClient())
    {
        HttpResponseMessage response = await client.GetAsync(url).ConfigureAwait(false);

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
        }

        string result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        return new string[] { result }; // Return as array if needed
    }
}

Using ConfigureAwait(false) tells the runtime that the continuation after the await doesn’t need to run on the original synchronization context. This prevents the deadlock because the continuation can run on any available thread pool thread, not the blocked original context.

According to the Microsoft ConfigureAwait FAQ, “If instead the library method had used ConfigureAwait(false), it would not queue the callback back to the original context, avoiding the deadlock scenarios.”

However, this approach has some limitations:

  • It only works if your calling code is truly independent of the synchronization context
  • If you need to update UI elements or access context-specific resources after the await, you can’t use ConfigureAwait(false)
  • It doesn’t solve the underlying architectural problem of mixing async and blocking code

Solution 3: Using IHttpClientFactory

The HTTP client creation pattern in your code also has issues. Creating a new HttpClient instance for each request can lead to socket exhaustion problems because sockets remain in the TIME_WAIT state after use. The recommended approach is to use IHttpClientFactory, which manages the lifecycle of HttpClient instances properly.

First, register IHttpClientFactory in your Startup.cs or Program.cs:

csharp
// In Startup.cs or Program.cs
services.AddHttpClient();

Then modify your code to use the factory:

csharp
private readonly IHttpClientFactory _httpClientFactory;

public YourService(IHttpClientFactory httpClientFactory)
{
    _httpClientFactory = httpClientFactory;
}

private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        HttpResponseMessage response = await client.GetAsync(url);

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
        }

        string result = await response.Content.ReadAsStringAsync();
        return new string[] { result }; // Return as array if needed
    }
}

The Microsoft documentation on HttpClient guidelines strongly recommends this approach: “HttpClient instance with PooledConnectionLifetime set to the desired interval, such as 2 minutes, depending on expected DNS changes.”

Benefits of using IHttpClientFactory include:

  • Proper management of HttpClient instances
  • Socket reuse to prevent exhaustion
  • Configuration management for named clients
  • Logging and telemetry integration
  • Better handling of DNS changes

If you need to support DNS changes in your scenario, you can configure the handler lifetime:

csharp
services.AddHttpClient("geoapify", client =>
{
    client.BaseAddress = new Uri("https://api.geoapify.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() => 
    new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2) });

As Milan Jovanovic explains, “If you want to use a typed client in a singleton service, the recommended approach is using SocketsHttpHandler as the primary handler, and configuring the PooledConnectionLifetime.”

Additional Best Practices

1. Use Static HttpClient for Console Applications

For console applications where dependency injection isn’t available, create a static HttpClient instance:

csharp
private static readonly HttpClient _httpClient = new HttpClient();

private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    HttpResponseMessage response = await _httpClient.GetAsync(url);

    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
    }

    string result = await response.Content.ReadAsStringAsync();
    return new string[] { result };
}

The Software Engineering Stack Exchange discussion confirms this approach: “Short answer: use a static HttpClient.”

2. Handle Exceptions Properly

Your current code throws an exception for non-success status codes, but you should also handle other potential exceptions:

csharp
private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        try
        {
            HttpResponseMessage response = await client.GetAsync(url);

            if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
            }

            string result = await response.Content.ReadAsStringAsync();
            return new string[] { result };
        }
        catch (HttpRequestException ex)
        {
            // Handle HTTP request exceptions
            throw;
        }
        catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
        {
            // Handle timeouts
            throw new TimeoutException("The request timed out");
        }
        catch (Exception ex)
        {
            // Handle other exceptions
            throw new ApplicationException("An error occurred while fetching location data", ex);
        }
    }
}

3. Use Strongly Typed Models

Instead of returning a string array, consider using a strongly typed model:

csharp
public class GeoapifyResult
{
    // properties matching your API response
}

private async Task<GeoapifyResult> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        HttpResponseMessage response = await client.GetAsync(url);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadFromJsonAsync<GeoapifyResult>();
    }
}

This provides better type safety and makes your code more maintainable.

Handling Timeouts and Cancellation

Another common reason for HttpClient calls hanging is timeouts. By default, HttpClient has a timeout of 100 seconds, but you might need to adjust this for your specific use case.

Setting Explicit Timeouts

csharp
private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        // Set a 30-second timeout
        client.Timeout = TimeSpan.FromSeconds(30);
        
        HttpResponseMessage response = await client.GetAsync(url);
        // ...
    }
}

According to Jose Javier Columbie’s HttpClient Best Practices, “The default timeout is 100 seconds. Adjusting the timeout can prevent your application from hanging indefinitely on requests.”

Using Cancellation Tokens

For better control, use a CancellationToken:

csharp
private async Task<string[]> GetLocation(double lat, double lon, CancellationToken cancellationToken = default)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        client.Timeout = TimeSpan.FromSeconds(30);
        
        try
        {
            HttpResponseMessage response = await client.GetAsync(url, cancellationToken);
            // ...
        }
        catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested)
        {
            // Operation was cancelled
            throw new OperationCanceledException("The operation was cancelled", ex, cancellationToken);
        }
    }
}

// Usage with timeout
public async Task<string[]> GetLocationWithTimeout(double lat, double lon, int timeoutSeconds = 30)
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
    
    try
    {
        return await GetLocation(lat, lon, cts.Token);
    }
    catch (OperationCanceledException)
    {
        throw new TimeoutException($"The request timed out after {timeoutSeconds} seconds");
    }
}

As Rajat Awasthi explains, “What we need to do next is straightforward. We start by initializing a CancellationTokenSource and setting a timeout using the CancelAfter() method. Then, we pass this token to the HTTPClient’s SendAsync method.”

Common Mistakes to Avoid

  1. Blocking on async code: Never use .Result, .Wait(), or Task.Run().Result() as this causes deadlocks.

  2. Creating HttpClient instances per request: This leads to socket exhaustion. Use IHttpClientFactory or a static instance.

  3. Not handling timeouts: Set appropriate timeouts based on your expected response times.

  4. Ignoring cancellation tokens: Always support cancellation for better resource management.

  5. Misusing async void: Only use async void for event handlers. For all other cases, use async Task.

  6. Not using ConfigureAwait(false) in library code: This can cause deadlocks when consumed by UI or ASP.NET applications.

  7. Not disposing HttpClient properly: While HttpClient itself doesn’t need disposal (it’s designed to be reused), you should dispose the HttpResponseMessage content.

Testing and Debugging Strategies

When debugging hanging HttpClient calls:

  1. Check for deadlocks: Use the debugger to see if threads are waiting on each other.

  2. Monitor network activity: Use tools like Fiddler or Wireshark to see if requests are actually being sent.

  3. Check for timeout issues: Log the time before and after the call to identify long-running operations.

  4. Use diagnostic tools: The Microsoft Diagnostics Tools can help identify hanging threads.

  5. Implement retry logic: For transient failures, implement exponential backoff retry:

csharp
private async Task<string[]> GetLocationWithRetry(double lat, double lon, int maxRetries = 3)
{
    int retryCount = 0;
    
    while (retryCount < maxRetries)
    {
        try
        {
            return await GetLocation(lat, lon);
        }
        catch (HttpRequestException ex) when (retryCount < maxRetries - 1)
        {
            retryCount++;
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
        }
    }
    
    throw new HttpRequestException($"Failed after {maxRetries} attempts");
}
  1. Log detailed information: Add logging to track request/response details:
csharp
private readonly ILogger<YourService> _logger;

private async Task<string[]> GetLocation(double lat, double lon)
{
    string url = $"https://api.geoapify.com/v1/geocode/reverse?lat={lat}&lon={lon}&format=json&apiKey={apiKey}";
    
    _logger.LogInformation("Making request to Geoapify API: {Url}", url);
    
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        try
        {
            HttpResponseMessage response = await client.GetAsync(url);
            
            _logger.LogInformation("Received response with status: {StatusCode}", response.StatusCode);
            
            if (!response.IsSuccessStatusCode)
            {
                string errorContent = await response.Content.ReadAsStringAsync();
                _logger.LogError("API returned error: {StatusCode} - {ErrorContent}", 
                    response.StatusCode, errorContent);
                
                throw new HttpRequestException($"Geoapify client.GetAsync request failed with status code {response.StatusCode}");
            }

            string result = await response.Content.ReadAsStringAsync();
            _logger.LogInformation("Successfully retrieved location data");
            
            return new string[] { result };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while fetching location data");
            throw;
        }
    }
}

Summary of Solutions

To fix the hanging HttpClient call in your C# application, implement these solutions:

  1. Make your calling code fully async:

    csharp
    // Instead of:
    string[] loc = taskRes.Result;
    
    // Use:
    string[] loc = await GetLocation(ap.Latitude, ap.Longitude);
    
  2. Use IHttpClientFactory for proper HTTP client management:

    csharp
    // Register in Startup.cs/Program.cs
    services.AddHttpClient();
    
    // Use in your service
    private readonly IHttpClientFactory _httpClientFactory;
    
    private async Task<string[]> GetLocation(double lat, double lon)
    {
        using (HttpClient client = _httpClientFactory.CreateClient())
        {
            // Your HTTP call code here
        }
    }
    
  3. Add proper exception handling and timeouts:

    csharp
    using (HttpClient client = _httpClientFactory.CreateClient())
    {
        client.Timeout = TimeSpan.FromSeconds(30);
        
        try
        {
            HttpResponseMessage response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();
            
            string result = await response.Content.ReadAsStringAsync();
            return new string[] { result };
        }
        catch (Exception ex)
        {
            // Handle exceptions appropriately
            throw;
        }
    }
    
  4. For library code, use ConfigureAwait(false):

    csharp
    HttpResponseMessage response = await client.GetAsync(url).ConfigureAwait(false);
    string result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    

By implementing these solutions, you’ll eliminate the deadlock issue, prevent socket exhaustion, and create more robust HTTP client code in your C# applications.

Sources

Conclusion

Your HttpClient call hangs due to a classic synchronization context deadlock caused by mixing async and blocking code. The primary solution is to make your entire call chain async by using await instead of .Result. Additionally, implement proper HTTP client management using IHttpClientFactory, set appropriate timeouts, and handle exceptions effectively. By following these best practices, you’ll eliminate hanging calls, prevent resource exhaustion, and create more robust C# applications that interact reliably with external APIs.

Authors
Verified by moderation
Moderation
C# HttpClient Deadlock: Fix Hanging API Calls