Introduction
In this chapter, we'll cover:
- Sync-over-async problems
- Deadlocks
- Async void disasters
- Task.Run misuse
- Blocking shared thread pool
- Misconfigured ValueTask
- Forgetting ConfigureAwait
- Hidden backpressure & queuing issues
- Async in constructors
- Overawaiting & underawaiting
- I/O vs CPU confusion
- Returning cold tasks incorrectly
All of these cause real production outages if ignored.
10.1 The #1 Async Killer: Sync-over-Async (Blocking .Result / .Wait())
❌ Dangerous Code
var user = _service.GetUserAsync(id).Result;
or:
_service.SaveAsync().Wait();
Why This Breaks:
- It blocks the calling thread
- Can cause deadlocks
- Causes ThreadPool starvation
- Cancels scalability benefits of async
- Makes async code behave like slow synchronous code
Always Use:
var user = await _service.GetUserAsync(id);
10.2 The Classic Deadlock Scenario (Very Common in .NET)
❌ This code DEADLOCKS:
public User GetUser()
{
return _repo.GetUserAsync().Result; // Deadlock
}
Why?
- The .Result blocks the thread
- await inside GetUserAsync() tries to resume on the same context
- That context is blocked → DEADLOCK
Fix:
public async Task<User> GetUserAsync()
{
return await _repo.GetUserAsync();
}
10.3 Using async void: The Worst Anti-Pattern
❌ Never do this:
public async void Process()
{
await Task.Delay(1000);
}
Reasons:
- Exceptions cannot be caught
- Crash the process silently
- Impossible to test
- Breaks reliability
- Not awaitable
Only acceptable place for async void:
✔ Event handlers (UI events in desktop apps—NOT in APIs)
10.4 Misusing Task.Run in ASP.NET Core
❌ Never offload I/O work to Task.Run
await Task.Run(() => _db.SaveChanges()); // WRONG
Why?
- Wastes a pool thread
- Adds expensive thread switch
- Breaks scalability
- Causes thread starvation under load
Only use Task.Run for:
- ✔ CPU-bound parallel workloads in non-ASP.NET apps
- ✔ Background worker apps (not API request pipeline)
APIs must rely on async I/O, not Task.Run.
10.5 Blocking the ThreadPool (Huge Production Issue)
❌ Dangerous patterns:
Thread.Sleep(1000); // Blocks thread
Task.Delay(1000).Wait(); // Blocks thread
Effects:
- Ties up server threads
- Incoming requests queue
- High latency
- Server timeouts
Always use:
await Task.Delay(1000);
10.6 Forgetting to Propagate CancellationToken
❌ Wrong
public Task<List<Order>> GetOrdersAsync()
{
return _db.Orders.ToListAsync(); // No token
}
Why it's critical:
- Long-running DB queries continue after client disconnect
- Resources wasted
- Orphaned background work
- May overload DB under high traffic
Correct:
public Task<List<Order>> GetOrdersAsync(CancellationToken token)
{
return _db.Orders.ToListAsync(token);
}
10.7 Not Using ConfigureAwait in Library Code
What ConfigureAwait(false) Does:
- Avoids capturing synchronization context
- Reduces deadlock risk
- Improves performance
- Recommended for libraries (not web apps)
Example:
await SomeInternalAsyncMethod().ConfigureAwait(false);
⚠️ In ASP.NET Core, synchronization context does not exist
→ So ConfigureAwait doesn't affect correctness, only micro-performance.
10.8 Misunderstanding ValueTask (Can Break Your App)
ValueTask is tricky.
❌ Mistake 1: Returning ValueTask without need
public ValueTask<int> GetAsync()
{
return new ValueTask<int>(_repo.GetCountAsync()); // WRONG
}
This is actually worse than using Task.
❌ Mistake 2: Consuming ValueTask multiple times
var x = await valueTask;
var y = await valueTask; // FAIL – ValueTask may not be reusable
When ValueTask should be used:
- ✔ High-performance hot paths
- ✔ Frequently synchronous completion
- ✔ Avoiding allocations
❌ NOT for general async code
10.9 Fire-and-Forget Inside Controllers (Silent Failures)
❌ Wrong:
Task.Run(() => _service.SendEmailAsync()); // No await, no logging, no retry
return Ok();
Problems:
- Task may crash
- No exception handling
- Lost logs
- Worker may die during app shutdown
- Data inconsistency
Correct:
Use a background queue or IHostedService (covered in Chapter 9).
10.10 Writing Async Code Inside Constructors (Not Allowed)
Constructors cannot be async.
❌ Wrong:
public class Foo
{
public Foo()
{
await InitAsync(); // Invalid
}
}
Correct patterns:
- ✔ Use async initialization method
- ✔ Use factory method
- ✔ Use Lazy<Task<T>>
10.11 Serial Await When Parallelism Is Needed
❌ Slow code:
var a = await GetAAsync();
var b = await GetBAsync();
var c = await GetCAsync();
Runs sequentially → slow.
✔ Correct:
var aTask = GetAAsync();
var bTask = GetBAsync();
var cTask = GetCAsync();
await Task.WhenAll(aTask, bTask, cTask);
var a = aTask.Result;
var b = bTask.Result;
var c = cTask.Result;
Huge performance win.
10.12 Parallel ForEach DOES NOT support async by default
❌ Wrong:
Parallel.ForEach(users, async user =>
{
await ProcessUserAsync(user);
});
This creates:
- Unobserved tasks
- Lost exceptions
- Messy behavior
✔ Correct pattern:
await Task.WhenAll(users.Select(u => ProcessUserAsync(u)));
10.13 Over-Awaiting (await inside tight loops)
❌ Slow:
foreach (var item in items)
{
await Process(item);
}
✔ Better:
await Task.WhenAll(items.Select(item => Process(item)));
10.14 Not Handling Exceptions inside WhenAll
❌ Wrong:
await Task.WhenAll(tasks); // exceptions aggregated silently
Correct:
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// ex.InnerExceptions contains all failures
}
10.15 Async Without Async (Fake Async / Fake I/O)
❌ Wrong:
public async Task<int> FakeAsync()
{
Thread.Sleep(1000); // blocking
return 1;
}
This is worse than synchronous.
✔ Correct:
public async Task<int> RealAsync()
{
await Task.Delay(1000);
return 1;
}
10.16 Returning Cold Tasks Incorrectly
❌ Wrong:
public Task<int> GetValue() => new Task<int>(() => 42);
This returns a non-started task. Consumers get stuck.
✔ Correct:
public Task<int> GetValue() => Task.FromResult(42);
10.17 Forgetting async all the way down
❌ Wrong pattern:
Controller: async
Service: sync
Repository: sync
DB: sync
Async only at the top does nothing.
✔ Correct:
Controller → Service → Repository → DB → Network
Every layer must be async.
10.18 Large Object Allocations in Async Loops
Allocating large lists inside async loops causes memory pressure.
❌ Wrong:
foreach (...)
var buffer = new byte[1024 * 1024];
Use pooling:
ArrayPool<byte>.Shared.Rent(...)
10.19 Common Performance Mistakes
❌ Using async when not needed (CPU-bound work)
Async is useless for CPU-only logic.
Use:
- Parallel.ForEach
- ValueTask
- Task.Run in background workers (NOT in APIs)
10.20 The Ultimate Anti-Pattern: Mixing async + blocking + sync
This is how outages happen:
await _repo.GetAsync();
var data = _http.GetAsync().Result; // mixed await + sync → DEADLOCK
_SlowFileWrite().Wait(); // blocked thread
Never mix sync and async.
10.21 Async Best Practices Checklist
- ✔ Always use async all the way down
- ✔ Never use .Result or .Wait()
- ✔ Avoid async void
- ✔ Don't use Task.Run in controllers
- ✔ Use CancellationToken everywhere
- ✔ Use Task.WhenAll for concurrency
- ✔ Use ValueTask only in optimized paths
- ✔ Avoid blocking calls inside async loops
- ✔ Always await async tasks
- ✔ Stream large data instead of loading fully
- ✔ Handle aggregated exceptions properly
- ✔ Never hide exceptions
- ✔ Never fire-and-forget in API pipeline
- ✔ Never block ThreadPool threads
10.22 Thread Pool Starvation: Diagnosis and Prevention
How to Detect Thread Pool Starvation
// Diagnostic method to check thread pool health
public static void LogThreadPoolStatus(ILogger logger)
{
ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
var workerUsage = ((double)(maxWorkerThreads - workerThreads) / maxWorkerThreads) * 100;
var ioUsage = ((double)(maxCompletionPortThreads - completionPortThreads) / maxCompletionPortThreads) * 100;
logger.LogWarning(
"ThreadPool status: Worker threads {UsedWorker}/{MaxWorker} ({WorkerPct}%), " +
"IOCP threads {UsedIO}/{MaxIO} ({IOPct}%)",
maxWorkerThreads - workerThreads, maxWorkerThreads, workerUsage,
maxCompletionPortThreads - completionPortThreads, maxCompletionPortThreads, ioUsage);
}
Warning signs:
- Worker threads usage > 80%
- IOCP threads usage > 80%
- Queue length growing continuously
- Increasing response times under load
Thread Pool Injection Anti-Pattern
// ❌ WRONG: Causes thread pool injection (slow and dangerous)
public async Task ProcessManyItemsAsync(List<Item> items)
{
var tasks = items.Select(item => Task.Run(async () =>
{
// Each Task.Run creates new thread pool work item
await ProcessItemAsync(item);
}));
await Task.WhenAll(tasks); // Thread pool injection!
}
// ✅ CORRECT: Use natural async parallelism
public async Task ProcessManyItemsCorrectAsync(List<Item> items)
{
var tasks = items.Select(item => ProcessItemAsync(item));
await Task.WhenAll(tasks); // No thread pool injection
}
Thread Pool Configuration Tuning
// Programmatic thread pool configuration (use cautiously!)
public static void ConfigureThreadPool()
{
// Only adjust if you have concrete evidence of starvation
ThreadPool.SetMinThreads(
workerThreads: 100,
completionPortThreads: 100);
// Monitor for 1-2 minutes before deciding to adjust
// Defaults are usually fine for most applications
}
10.23 Async Lock Patterns and Deadlocks
❌ The Async Lock Deadlock Pattern
private readonly object _syncLock = new object();
public async Task<int> GetValueAsync()
{
lock (_syncLock) // ❌ Blocking lock in async method
{
var value = await GetValueFromDbAsync(); // Deadlock risk
return Process(value);
}
}
✅ Correct: Async-compatible locks
// Option 1: SemaphoreSlim (preferred for async)
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
public async Task<int> GetValueAsync()
{
await _asyncLock.WaitAsync();
try
{
var value = await GetValueFromDbAsync();
return Process(value);
}
finally
{
_asyncLock.Release();
}
}
// Option 2: AsyncLock from Nito.AsyncEx
private readonly AsyncLock _asyncLock = new AsyncLock();
public async Task<int> GetValueAsync()
{
using (await _asyncLock.LockAsync())
{
var value = await GetValueFromDbAsync();
return Process(value);
}
}
Deadlock Detection with Timeout
public async Task<T> ExecuteWithTimeoutAsync<T>(
Func<Task<T>> operation,
TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
try
{
return await operation().WaitAsync(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new TimeoutException($"Operation timed out after {timeout}");
}
}
// Usage
await ExecuteWithTimeoutAsync(
() => _repository.GetDataAsync(),
TimeSpan.FromSeconds(5));
10.24 Task Continuation Pitfalls
❌ Wrong: Continuation that blocks
// This continuation runs on thread pool but blocks
var task = SomeAsyncOperation()
.ContinueWith(previousTask =>
{
// ❌ Blocking call in continuation
var result = previousTask.Result;
Thread.Sleep(1000); // Blocks thread pool thread
return Process(result);
});
✅ Correct: Async continuation
// Use ContinueWith with TaskContinuationOptions
var task = SomeAsyncOperation()
.ContinueWith(async previousTask =>
{
// ✅ Proper async continuation
var result = await previousTask;
await Task.Delay(1000); // Non-blocking
return await ProcessAsync(result);
}, TaskContinuationOptions.OnlyOnRanToCompletion)
.Unwrap(); // Important: Unwrap nested task
Continuation Context Capture
// ❌ Captures synchronization context (deadlock risk in WinForms/WPF)
var task = SomeAsyncOperation()
.ContinueWith(t =>
{
UpdateUI(); // Runs on UI thread
}, TaskScheduler.FromCurrentSynchronizationContext());
// ✅ Use ConfigureAwait in async methods instead
public async Task ProcessAsync()
{
var result = await SomeAsyncOperation().ConfigureAwait(false);
// Run continuation on thread pool
await ProcessResultAsync(result);
}
10.25 Async State Machine Performance Overhead
The Hidden Cost of Async State Machines
// Each async method creates a state machine allocation
public async Task<int> ExpensiveAsync()
{
// State machine allocation: ~100 bytes
var a = await GetAAsync(); // First await
var b = await GetBAsync(); // Second await
return a + b;
}
// Hot path optimization: Avoid async for simple cases
public Task<int> OptimizedAsync()
{
// No state machine if method only calls other async methods
return GetSumAsync();
}
private async Task<int> GetSumAsync()
{
var aTask = GetAAsync();
var bTask = GetBAsync();
await Task.WhenAll(aTask, bTask);
return aTask.Result + bTask.Result;
}
When to Avoid Async
// ❌ Unnecessary async overhead
public async Task<string> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out string value))
return value; // Async state machine for synchronous path
value = await GetFromSourceAsync(key);
_cache[key] = value;
return value;
}
// ✅ Optimized: Separate sync and async paths
public Task<string> GetCachedValueOptimizedAsync(string key)
{
if (_cache.TryGetValue(key, out string value))
return Task.FromResult(value); // No async overhead
return GetCachedValueInternalAsync(key);
}
private async Task<string> GetCachedValueInternalAsync(string key)
{
var value = await GetFromSourceAsync(key);
_cache[key] = value;
return value;
}
10.26 Async Dispose Patterns
❌ Wrong: Blocking dispose in async context
public class Resource : IDisposable
{
public void Dispose()
{
// ❌ Blocking I/O in dispose
_stream.Flush(); // May block
_stream.Dispose();
}
}
// Usage that causes issues
await using (var resource = new Resource())
{
await resource.DoSomethingAsync();
} // Dispose() blocks here
✅ Correct: IAsyncDisposable pattern
public class AsyncResource : IAsyncDisposable
{
private readonly Stream _stream;
public async ValueTask DisposeAsync()
{
await _stream.FlushAsync(); // Non-blocking
await _stream.DisposeAsync();
}
}
// Usage with await using
await using (var resource = new AsyncResource())
{
await resource.DoSomethingAsync();
} // DisposeAsync() called asynchronously
Mixed Sync/Async Dispose
public class DualDisposeResource : IDisposable, IAsyncDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose synchronous resources
_syncResource?.Dispose();
}
// Dispose unmanaged resources
_disposed = true;
}
}
protected virtual async ValueTask DisposeAsyncCore()
{
if (!_disposed)
{
// Dispose asynchronous resources
if (_asyncResource != null)
await _asyncResource.DisposeAsync();
_disposed = true;
}
}
}
10.27 Task Completion Source Misuse
❌ Wrong: TCS without proper completion
public Task<int> GetValueAsync()
{
var tcs = new TaskCompletionSource<int>();
// ❌ Forgetting to complete the task
_eventSource.OnValueReceived += value =>
{
// What if this never fires?
// Task never completes!
};
return tcs.Task; // Task may never complete
}
✅ Correct: TCS with timeout and cancellation
public async Task<int> GetValueWithTimeoutAsync(
CancellationToken cancellationToken,
TimeSpan timeout)
{
var tcs = new TaskCompletionSource<int>();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(timeout);
using var registration = timeoutCts.Token.Register(() =>
tcs.TrySetCanceled(timeoutCts.Token));
_eventSource.OnValueReceived += value =>
tcs.TrySetResult(value);
_eventSource.OnError += error =>
tcs.TrySetException(new InvalidOperationException(error));
return await tcs.Task;
}
TCS Memory Leak Pattern
// ❌ Memory leak: TCS kept alive by event handler
public class LeakyService
{
private readonly List<TaskCompletionSource<int>> _pendingTasks = new();
public Task<int> WaitForValueAsync()
{
var tcs = new TaskCompletionSource<int>();
_pendingTasks.Add(tcs); // ❌ Never removed
_eventSource.OnValue += value =>
{
tcs.TrySetResult(value);
// ❌ Should remove from list but doesn't
};
return tcs.Task;
}
}
// ✅ Correct: Clean up references
public Task<int> WaitForValueCleanAsync()
{
var tcs = new TaskCompletionSource<int>();
var weakReference = new WeakReference<TaskCompletionSource<int>>(tcs);
EventHandler handler = null;
handler = (sender, value) =>
{
if (weakReference.TryGetTarget(out var targetTcs))
{
targetTcs.TrySetResult(value);
_eventSource.OnValue -= handler; // Clean up
}
};
_eventSource.OnValue += handler;
return tcs.Task;
}
10.28 Async Local and Execution Context Pitfalls
❌ AsyncLocal Leakage
private static readonly AsyncLocal<int> _asyncLocalValue = new();
public async Task ProcessAsync()
{
_asyncLocalValue.Value = 42;
await Task.Delay(100);
// ❌ Value persists across async boundaries
// May leak to other operations on same thread
Console.WriteLine(_asyncLocalValue.Value); // Still 42
await Task.Run(() =>
{
// ❌ May not propagate correctly
Console.WriteLine(_asyncLocalValue.Value);
});
}
✅ Correct AsyncLocal Usage
public class ScopedAsyncLocal<T> : IDisposable
{
private readonly AsyncLocal<T> _asyncLocal;
private readonly T _previousValue;
public ScopedAsyncLocal(AsyncLocal<T> asyncLocal, T value)
{
_asyncLocal = asyncLocal;
_previousValue = asyncLocal.Value;
asyncLocal.Value = value;
}
public void Dispose()
{
_asyncLocal.Value = _previousValue; // Restore previous value
}
}
// Usage
private static readonly AsyncLocal<string> _correlationId = new();
public async Task ProcessWithScopeAsync()
{
using (new ScopedAsyncLocal<string>(_correlationId, Guid.NewGuid().ToString()))
{
await DoWorkAsync(); // Has correlation ID
} // Correlation ID automatically cleared
await DoOtherWorkAsync(); // No correlation ID
}
10.29 ValueTask Double Await Protection
The Double Await Problem
public async ValueTask<int> GetValueAsync()
{
if (_cache.TryGetValue(out int value))
return value; // Returns ValueTask wrapping int
value = await FetchFromSourceAsync();
_cache.Set(value);
return value; // Returns ValueTask wrapping int
}
// ❌ DANGER: Double await
var valueTask = GetValueAsync();
var result1 = await valueTask;
var result2 = await valueTask; // May throw or return wrong value
✅ Safe ValueTask Patterns
// Pattern 1: Convert to Task after first await
public async Task<int> GetValueAsTaskAsync()
{
if (_cache.TryGetValue(out int value))
return value;
value = await FetchFromSourceAsync();
_cache.Set(value);
return value;
}
// Pattern 2: Document non-reusability
/// <summary>
/// Returns a ValueTask that should be awaited only once.
/// Do not store or await multiple times.
/// </summary>
public ValueTask<int> GetValueOnceAsync()
{
// Implementation
}
// Pattern 3: Use AsTask extension
var valueTask = GetValueAsync();
var task = valueTask.AsTask(); // Convert to reusable Task
var result1 = await task;
var result2 = await task; // Safe (if you really need this)
10.30 Async Method Builder Customization
Custom Async Method Builders (Advanced)
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
public async ValueTask<T> GetPooledAsync<T>()
{
// Uses pooled state machines to reduce allocations
await Task.Delay(1);
return default(T);
}
// Custom builder implementation (simplified)
public struct PoolingAsyncValueTaskMethodBuilder<T>
{
private PooledStateMachine<T> _stateMachine;
public static PoolingAsyncValueTaskMethodBuilder<T> Create()
{
return new PoolingAsyncValueTaskMethodBuilder<T>
{
_stateMachine = StateMachinePool<T>.Rent()
};
}
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
// Pool-aware state machine startup
}
// Other builder methods...
}
Chapter 10 Summary
You now understand the most dangerous async mistakes that break APIs in production:
- Deadlocks
- Thread starvation
- Unbounded queues
- Parallel.ForEach async misuse
- Async void exceptions
- Task.Run misuse
- Misbehaving ValueTasks
- Fake async I/O
- Underawaiting & overawaiting
- Not using CancellationToken
- Fire-and-forget tasks inside controllers
Mastering these prevents:
- Outages
- CPU spikes
- Memory leaks
- Broken workflows
- 500 errors
- Lost emails/jobs
- Production incidents
🎉 Chapter 10 (Deep Dive) is complete.
You're now ready for the next major chapter:
👉 Chapter 11: Resilience, Idempotency & Retry Patterns in Async APIs
This chapter will cover:
- Idempotency keys (Stripe-style)
- Safe retries in distributed systems
- Preventing duplicate insertion
- Detecting retry loops
- Optimistic concurrency
- Using Polly advanced patterns (Bulkhead, Timeouts, Fallback)
- Consistent API resilience design
Would you like me to proceed with Chapter 11 (Deep Dive)?