Introduction
In this article, we will learn about 10 async mistakes that can kill an ASP.NET Core application.
Before we start, please take a look at my last article.
Let's get started.
1. Blocking on Async Code (.Result, .Wait())
Mistake
var data = httpClient.GetStringAsync(url).Result; // ❌
Problem: This blocks the thread while waiting for the async task, potentially causing thread starvation and deadlocks under load.
Fix
var data = await httpClient.GetStringAsync(url); // ✅
Always use await all the way down the call chain.
2. Mixing Sync and Async Code
Mistake
public IActionResult GetData()
{
var data = GetDataAsync().Result;
return Ok(data);
}
Problem: ASP.NET Core uses an async pipeline. Blocking calls in controllers defeats the purpose of async I/O and can freeze requests.
Fix
public async Task<IActionResult> GetData()
{
var data = await GetDataAsync();
return Ok(data);
}
3. Not Using ConfigureAwait(false) in Libraries
Mistake
If you write a reusable library that uses async/await, but you rely on the synchronization context:
await SomeOperationAsync(); // ❌
Problem: In ASP.NET Core it’s less critical (no SynchronizationContext), but in shared code or desktop apps, it can cause context-capturing issues.
Fix
await SomeOperationAsync().ConfigureAwait(false); // ✅
4. Fire-and-Forget Tasks
Mistake
Task.Run(() => DoSomethingAsync()); // ❌
Problem: The task is unobserved — if it throws, the exception is lost or crashes the process.
Fix
If you must run background work:
_ = Task.Run(async () =>
{
try { await DoSomethingAsync(); }
catch (Exception ex) { _logger.LogError(ex, "Background error"); }
});
5. Over-Awaiting Small Tasks (Async Overhead)
Mistake
Making everything async “just because”:
public async Task<int> AddAsync(int a, int b)
{
return a + b; // ❌ no real async work
}
Problem: Adds overhead for no reason. Async has context-switch costs.
Fix: Keep it synchronous when no real async I/O is performed.
6. Creating Too Many HttpClient Instances
Mistake
var client = new HttpClient();
var response = await client.GetAsync(url);
Problem: Causes socket exhaustion and memory leaks.
Fix: Use IHttpClientFactory:
public MyService(HttpClient httpClient)
{
_httpClient = httpClient;
}
Register with:
services.AddHttpClient<MyService>();
7. Using Task.Run to “Make” Things Async
Mistake
var result = await Task.Run(() => SomeBlockingDatabaseCall());
Problem: Moves blocking code off-thread but doesn’t solve scalability — wastes thread pool threads.
Fix: Make the underlying operation truly async (e.g., EF Core’s ToListAsync())
8. Ignoring Cancellation Tokens
Mistake
public async Task ProcessRequest()
{
await Task.Delay(5000);
}
Problem: Ignores request cancellation (like when a client disconnects).
Fix
public async Task ProcessRequest(CancellationToken token)
{
await Task.Delay(5000, token);
}
Always respect HttpContext.RequestAborted.
9. Unobserved Task Exceptions
Mistake
_ = SomeAsyncOperation(); // ❌ exception may crash process
Problem: Exceptions in async methods not awaited can bring down your app.
Fix
Always await tasks or handle their exceptions with care.
10. Not Profiling or Measuring Async Performance
Mistake
Assuming async = faster.
Problem: Async helps scalability, not necessarily speed. Excessive async overhead can slow CPU-bound paths.
Fix: Measure using:
dotnet-trace
Application Insights
PerfView
BenchmarkDotNet
Bonus Tip: Always “async all the way”
If you start with an async method (like a controller action), propagate async through the entire call chain. Mixing sync/async is the #1 killer.
Conclusion
Here, we tried to cover async mistakes that can kill an ASP.NET Core application.