HCR080¶
High-concurrency HTTP fan-out should use bounded concurrency or connection limits.
Why¶
Task.WhenAll(items.Select(item => client.GetAsync(...))) can start unbounded outbound HTTP work. Under load this can exhaust sockets, queues, rate limits, or upstream capacity.
Bad¶
Better¶
await Parallel.ForEachAsync(urls, new ParallelOptions
{
MaxDegreeOfParallelism = 8,
CancellationToken = cancellationToken
}, async (url, ct) =>
{
using var response = await client.GetAsync(url, ct);
});
using var client = new HttpClient(new HttpClientHandler
{
MaxConnectionsPerServer = 8
});
await Task.WhenAll(urls.Select(url => client.GetAsync(url, cancellationToken)));
Current Detection¶
The implementation reports System.Threading.Tasks.Task.WhenAll(...) calls whose first argument contains a LINQ Select(...) lambda or semantically translated C# LINQ query with an obvious HttpClient async call, including response-body helpers, framework System.Net.Http.Json delete/read/write extensions, and visible local task sequences such as var tasks = urls.Select(...); await Task.WhenAll(tasks);. It follows the latest visible initializer or standalone assignment before WhenAll, treating parentheses and null-forgiving operators as transparent around task sequences, while conservatively skipping task-sequence writes nested in control flow. The WhenAll target, Select(...) call or translated query, and resolved HTTP method are validated against BCL symbols when available, and the HTTP call receiver must resolve to System.Net.Http.HttpClient or be visibly declared as HttpClient in unresolved source, so lookalike Task.WhenAll(...), custom Select(...) and query-pattern methods, resolved custom HttpClient types, custom extensions on HttpClient, and custom fan-out clients are skipped.
It does not report when the lambda visibly gates work with SemaphoreSlim.WaitAsync(...) and a matching Release() on the same symbol-equivalent or visibly declared SemaphoreSlim receiver, when code uses a bounded Parallel.ForEachAsync shape instead of Task.WhenAll, or when the called local, field, property, or this.-qualified member HttpClient is visibly constructed with an inline or member framework SocketsHttpHandler or HttpClientHandler whose initializer sets MaxConnectionsPerServer, including null-forgiving client and handler expressions. Local HttpClient and handler evidence must apply before those locals are reassigned; a later assigned client or handler without connection limits still reports. Lookalike custom gate types with WaitAsync(...) and Release(), and lookalike custom handler types with a MaxConnectionsPerServer property, do not suppress the diagnostic when Roslyn can resolve their symbols.
Suppression¶
Suppress when the input collection is already tightly bounded or concurrency is controlled outside the visible code.