Using async/await everywhere doesn’t automatically make your code faster – it can actually make it slower if done wrong. Here are the critical mistakes.
Mistake 1: Async All The Way Down (Unnecessarily):
// BAD: Unnecessary async overhead
public async Task GetUserIdAsync(string username)
{
return await Task.FromResult(_cache.Get(username)); // Why async?
}
// This creates a Task, schedules it, waits for it, unwraps it
// Just to return a cached value that's already available!
// GOOD: Don't use async for synchronous work
public int GetUserId(string username)
{
return _cache.Get(username); // Direct return
}
// Or if you MUST return Task (interface requirement):
public Task GetUserIdAsync(string username)
{
return Task.FromResult(_cache.Get(username)); // No async keyword
}
Performance Impact:
• Async method: ~50-100ns overhead per call
• Synchronous method: ~5ns
• In a hot path called 1M times: 100ms vs 5ms (20x difference)
Mistake 2: Blocking on Async Code (Deadlock Risk):
// BAD: Deadlock in ASP.NET
public ActionResult Index()
{
var data = GetDataAsync().Result; // DEADLOCK!
return View(data);
}
public async Task GetDataAsync()
{
var result = await _httpClient.GetAsync("https://api.example.com");
return await result.Content.ReadAsAsync();
}
// Why deadlock:
// 1. GetDataAsync awaits GetAsync
// 2. Continuation tries to resume on ASP.NET SynchronizationContext
// 3. .Result blocks the thread, which IS the SynchronizationContext thread
// 4. Continuation waits for blocked thread
// 5. Blocked thread waits for continuation
// = DEADLOCK
FIX – Option 1: Async All The Way:
// GOOD: Never block, always await
public async Task IndexAsync()
{
var data = await GetDataAsync(); // Properly await
return View(data);
}
FIX – Option 2: ConfigureAwait(false):
// If you MUST block (library code, not ASP.NET):
public Data GetData()
{
return GetDataAsync().GetAwaiter().GetResult(); // Better than .Result
}
public async Task GetDataAsync()
{
var result = await _httpClient.GetAsync("https://api.example.com")
.ConfigureAwait(false); // Don't capture context
return await result.Content.ReadAsAsync()
.ConfigureAwait(false);
}
Mistake 3: Fire-and-Forget Without Error Handling:
// BAD: Silent failures
public void ProcessOrder(int orderId)
{
_ = SendEmailAsync(orderId); // Fires task, ignores exceptions!
}
public async Task SendEmailAsync(int orderId)
{
await _emailService.SendAsync(...);
// If this throws, exception is LOST FOREVER
}
// GOOD: Proper fire-and-forget
public void ProcessOrder(int orderId)
{
_ = SendEmailWithHandlingAsync(orderId);
}
private async Task SendEmailWithHandlingAsync(int orderId)
{
try
{
await _emailService.SendAsync(...);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email for order {OrderId}", orderId);
// Maybe add to retry queue
await _retryQueue.AddAsync(orderId);
}
}
Mistake 4: Not Using Task.WhenAll for Parallel Operations:
// BAD: Sequential execution (slow)
public async Task<List> GetProducts(List productIds)
{
var products = new List();
foreach (var id in productIds)
{
var product = await _api.GetProductAsync(id); // Waits for each
products.Add(product);
}
return products;
}
// 10 products × 200ms each = 2000ms total
// GOOD: Parallel execution (fast)
public async Task<List> GetProducts(List productIds)
{
var tasks = productIds
.Select(id => _api.GetProductAsync(id))
.ToList();
var products = await Task.WhenAll(tasks);
return products.ToList();
}
// 10 products in parallel = 200ms total (10x faster!)
Mistake 5: Using Task.Run for I/O Operations:
// BAD: Wastes thread pool threads
public async Task GetDataAsync()
{
return await Task.Run(async () =>
{
return await _httpClient.GetAsync(url); // Why Task.Run?
});
}
// This:
// 1. Takes thread from pool to start Task.Run
// 2. That thread starts an async HTTP call
// 3. Thread returns to pool while waiting
// 4. Takes ANOTHER thread to process result
// = Wasted 2 threads for no reason
// GOOD: Just await I/O directly
public async Task GetDataAsync()
{
return await _httpClient.GetAsync(url); // No threads blocked
}
// Task.Run is ONLY for CPU-bound work:
public async Task CalculatePrimeNumbersAsync(int max)
{
return await Task.Run(() =>
{
// CPU-intensive loop here
return ComputePrimes(max); // Synchronous CPU work
});
}
Mistake 6: Returning Task Instead of ValueTask for High-Frequency Methods:
// BAD: Allocates Task on every call
public async Task TryGetFromCacheAsync(string key)
{
if (_cache.TryGetValue(key, out var value))
{
return true; // Still allocates Task
}
return await LoadFromDatabaseAsync(key);
}
// GOOD: Use ValueTask to avoid allocation when synchronous
public async ValueTask TryGetFromCacheAsync(string key)
{
if (_cache.TryGetValue(key, out var value))
{
return true; // No allocation!
}
return await LoadFromDatabaseAsync(key);
}
// Benchmark (1M calls, 90% cache hit rate):
// Task: 15MB allocated, 3 GC collections
// ValueTask: 1.5MB allocated, 0 GC collections
Decision Tree – When to Use What:
// Use async Task when:
// - Awaiting I/O operations (HTTP, DB, file)
// - Consumer needs to await the result
// - Not in a super hot path
// Use Task (no async keyword) when:
// - Returning already-completed tasks
// - Task.FromResult, Task.CompletedTask
// - Wrapping synchronous methods for interface compliance
// Use ValueTask when:
// - High-frequency method (called 1000s of times/sec)
// - Often returns synchronously (cache hits, validation)
// - Every allocation matters
// DON'T use async when:
// - Method is purely synchronous
// - Just wrapping Task.FromResult
// - Would cause unnecessary state machine generation
