A desktop screen freezes while loading a report. An API slows down while waiting for another service. Both can involve waiting, but adding async to a method is only part of the fix.
First identify what takes the time. A database request waits for I/O. A large calculation needs CPU time. Those need different decisions.
Await an existing asynchronous operation
This helper uses an HttpClient supplied by the caller. The caller owns and reuses that client; the helper disposes its response.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public static async Task<string> ReadResponseAsync(
HttpClient client,
Uri address,
CancellationToken cancellationToken)
{
using (var response = await client.GetAsync(address, cancellationToken))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
With the default GetAsync completion option, the response body is buffered before that await completes. The cancellation token applies to that request. A streaming implementation needs its own cancellation-aware body reading.
The calling code should also await the returned task. Blocking with .Result or .Wait() can hold a thread unnecessarily and can deadlock when a continuation needs a blocked UI context. A task represents an operation; it does not imply a dedicated thread.
Keep concurrency deliberate
Two independent HTTP requests can be started before awaiting Task.WhenAll. A dependent request must wait for the value it needs. Thousands of requests need a concurrency limit rather than one giant fan-out.
Do not apply this pattern to two queries on the same EF Core DbContext. That context does not support concurrent operations. Await each query or use separate contexts for work that can safely be independent.
Put calculation work in the right place
On a desktop UI, moving a genuinely expensive calculation into Task.Run can preserve responsiveness. On an ASP.NET Core request, wrapping ordinary database I/O in Task.Run adds scheduling work without removing the wait. Use the asynchronous database API instead.
A useful review question is: which operation is waiting, who owns its lifetime, and who observes its exception? Those answers matter more than the number of async keywords.
Read more in Microsoft's async scenarios and DbContext lifetime guidance.