C# Async/Await Interview Questions for Senior .NET Developers (2026)
From ConfigureAwait to ValueTask and Thread Pool Starvation โ Questions That Separate Good Developers from Great Ones

Asynchronous programming is one of the most tested and misunderstood areas in senior .NET interviews. Whether you are prepping for a principal engineer role or levelling up your team, mastering C# async/await goes far beyond slapping async Task on a method signature. Senior interviewers want to see that you understand how the state machine compiler transform works, when ConfigureAwait matters, how to avoid deadlocks in library code, and where ValueTask outperforms Task. This guide covers the questions that actually separate senior .NET developers from the rest.
๐ Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. ๐ https://www.patreon.com/CodingDroplets
Basic-Level Questions
What Does the async Keyword Actually Do to a Method?
The async keyword instructs the compiler to transform the method body into a state machine. The method itself does not automatically run on a background thread. The keyword only enables the use of await inside the method body and wraps the return value in a Task or Task<T>. If there are no await expressions inside, the method runs synchronously from start to finish โ the compiler will issue a warning about this, and senior candidates are expected to recognise it as a design smell.
The generated state machine tracks suspension and resumption points. Each await expression corresponds to a state transition. When the awaited operation completes, the scheduler resumes execution from the stored state, restoring the original context if one was captured.
What Is the Difference Between Task and ValueTask?
Task is a class allocated on the heap every time a method is called, even when the result is already available (e.g., a value returned from a cache). ValueTask is a struct that avoids the heap allocation when the operation completes synchronously.
The key distinction for senior developers: ValueTask must only be awaited once. It cannot be used for multiple consumers, stored for later await, or used with Task.WhenAll. Use ValueTask in hot-path code paths โ high-frequency API endpoints, caching layers โ where measurable allocation reduction is the goal. For general application code, Task is safer and easier to reason about.
What Is ConfigureAwait(false) and When Should You Use It?
ConfigureAwait(false) tells the awaiter not to capture the current synchronization context and resume on it. In UI frameworks (WPF, WinForms), the synchronization context schedules continuations back on the UI thread. In ASP.NET Core (pre-3.0), there was an ASP.NET synchronization context. In modern ASP.NET Core, there is no synchronization context โ ConfigureAwait(false) has no behavioral effect on the continuation.
The rule for senior developers:
- Library code: Always use
ConfigureAwait(false)to avoid deadlocks when callers block with.Resultor.Wait(). - Application code (ASP.NET Core): Optional. It has no effect on modern ASP.NET Core but signals intent to future readers and protects against accidental context-dependency.
- UI code: Never use
ConfigureAwait(false)in methods that need to return to the UI thread after the await.
Intermediate-Level Questions
How Does a Deadlock Occur with async/await and How Do You Prevent It?
Deadlocks in async code follow a predictable pattern. Imagine a synchronization context that allows only one thread at a time (classic ASP.NET or UI thread). A caller invokes an async method and blocks with .Result or .Wait(). The async method uses await and, by default, tries to resume on the original synchronization context. But that context is blocked by the .Result/.Wait() call, so neither side can proceed โ deadlock.
Prevention strategies:
- Never block on async code using
.Result,.Wait(), orGetAwaiter().GetResult()in library or shared code. - Use
ConfigureAwait(false)in library code to break the context dependency. - Use
asyncall the way up the call stack โ the "async all the way" principle. - If you must bridge sync-to-async code (e.g., legacy entry points), use
Task.Run(() => AsyncMethod()).GetAwaiter().GetResult()to offload to the thread pool where no synchronization context is present.
What Is the Difference Between Task.Run and async/await?
Task.Run explicitly queues work to the thread pool. It is designed for CPU-bound operations that would otherwise block the calling thread. async/await is designed for I/O-bound operations โ it releases the thread while waiting for an I/O result, without spinning up a new thread.
Common mistake: wrapping I/O-bound async code in Task.Run. This wastes a thread pool thread during the I/O wait because the thread cannot do anything while waiting. For ASP.NET Core APIs under load, this can saturate the thread pool and cause significant throughput degradation.
Correct usage:
- I/O-bound (database, HTTP, file): use
awaitdirectly withoutTask.Run. - CPU-bound (heavy computation): use
Task.Runto offload from the calling thread.
How Do CancellationToken and Async Methods Work Together?
CancellationToken is the standard mechanism for cooperative cancellation in .NET async code. You thread the token through the call stack โ from the top-level entry point (e.g., controller action) down to every I/O operation. Framework methods in HttpClient, DbContext, StreamReader, and others accept CancellationToken directly.
Key behaviours senior candidates must know:
- Cancellation is cooperative โ the token signals intent; the callee is responsible for observing it.
OperationCanceledExceptionis the expected exception on cancellation โ do not swallow it without logging.- In ASP.NET Core, the
HttpContext.RequestAbortedtoken is automatically cancelled when a client disconnects. Passing it to downstream calls prevents orphaned database queries and wasted compute. - Always check
cancellationToken.ThrowIfCancellationRequested()at the start of expensive loops or operations.
What Is WhenAll vs WhenAny and When Do You Use Each?
Task.WhenAll completes when all supplied tasks complete. It is the standard pattern for parallel I/O fan-out โ fire multiple independent requests simultaneously and wait for all results. It aggregates exceptions from all failed tasks into an AggregateException.
Task.WhenAny completes when any of the supplied tasks completes. It is used for timeout patterns, racing tasks, or responding to the first available result. The other tasks are not automatically cancelled โ you must cancel them explicitly via a CancellationTokenSource.
Common interview trap: what happens when one task in WhenAll throws? The awaited WhenAll will throw an exception from the first faulted task, but all tasks still run to completion (success, failure, or cancellation). Use a try/catch and inspect the task statuses individually if you need all failure details.
Advanced-Level Questions
What Is IAsyncEnumerable<T> and When Should You Prefer It Over Task<IEnumerable<T>>?
IAsyncEnumerable<T> (introduced in C# 8) enables streaming asynchronous sequences. Instead of loading all results into memory and returning a complete collection, the caller can consume items as they arrive โ a critical capability for large data sets, database cursors, and streaming API responses.
Use IAsyncEnumerable<T> when:
- The data source produces items incrementally (database query with
ToAsyncEnumerable, Kafka consumer, HTTP chunked response). - You need to process results with backpressure rather than buffering everything in memory.
- You are building pipelines where each stage can apply async operations per item.
Use Task<IEnumerable<T>> when the full result set is small, must be present before processing begins, or downstream callers expect a complete collection (e.g., caching, sorting, deduplication before returning).
In ASP.NET Core, returning IAsyncEnumerable<T> from a controller action enables JSON streaming โ the response starts immediately as results arrive rather than waiting for the full enumeration.
What Are the Differences Between async void, async Task, and async Task<T>?
async void is a fire-and-forget signature. It cannot be awaited. Exceptions thrown inside an async void method are propagated directly to the synchronization context and will crash the process if unhandled. The only legitimate use case is event handlers in UI frameworks โ and even there, wrapping the body in try/catch is mandatory.
async Task represents a void operation that can be awaited and observed for exceptions. This is the correct signature for any async method that does not return a value but should be composable and exception-observable.
async Task<T> represents a typed async result. It is the most common signature for async service and repository methods.
Senior candidates are expected to identify async void in code review and flag it as a risk unless it is a UI event handler with proper error handling.
How Does the Async State Machine Work Under the Hood?
The C# compiler rewrites every async method into a struct implementing IAsyncStateMachine. The original method body is split at each await point into state transitions. Key elements of the generated code:
- State field: integer tracking which suspension point the machine is at.
- MoveNext(): the core loop that drives execution from one state to the next.
- SetStateMachine(): registers the state machine with the async infrastructure.
- Awaiter storage: fields on the struct hold the awaiters for each await expression, avoiding allocations when possible.
For performance-critical library code, understanding this transformation matters because:
- Struct-based state machines (in Release builds) avoid an extra heap allocation.
ValueTaskandManualResetValueTaskSourceCore<T>allow completely allocation-free async paths in hot code.- Excessive try/catch blocks inside async methods force the compiler to use a less efficient code path.
What Is the Difference Between SemaphoreSlim.WaitAsync and lock in Async Code?
The lock statement is synchronous โ it blocks the thread if the lock is contended. Inside an async method, blocking a thread is an anti-pattern. If you call lock inside an async method and the lock is contended, you tie up a thread pool thread while waiting, which defeats the entire purpose of async.
SemaphoreSlim.WaitAsync() is the async-compatible alternative for mutual exclusion. It returns a Task that completes when the semaphore is acquired, releasing the thread during the wait. It also supports timeouts and cancellation tokens.
Pattern for async locking:
await _semaphore.WaitAsync(cancellationToken);
try { /* critical section */ }
finally { _semaphore.Release(); }
For read-heavy scenarios, ReaderWriterLockSlim does not have an async equivalent in the BCL โ use the AsyncReaderWriterLock from the Nito.AsyncEx package or implement a custom version with SemaphoreSlim.
Expert-Level Questions
How Do You Diagnose and Fix Thread Pool Starvation in an ASP.NET Core Application?
Thread pool starvation happens when all available thread pool threads are blocked waiting for I/O or locks rather than doing productive work. Symptoms include high latency, low CPU usage, and a growing queue of pending requests.
Root causes in async code:
- Synchronous blocking on async operations (
.Result,.Wait()) - Excessive
Task.Runwrapping of I/O-bound code - Poorly configured
MaxDegreeOfParallelismin parallel loops - Deadlocking awaits that chain-consume threads
Diagnostic approach:
- Capture a thread dump (via
dotnet-dumpor Process Explorer) โ look for large numbers of threads stuck in wait states. - Use
EventPipe+dotnet-countersto monitorThreadPool Queue LengthandThreadPool Thread Countin real time. - Review APM traces (Application Insights, OpenTelemetry) for synchronous blocking callsites.
Fix: eliminate all blocking-on-async patterns. Instrument with dotnet-counters monitor --counters System.Runtime to verify improvement.
What Is IAsyncDisposable and How Does It Differ from IDisposable?
IAsyncDisposable (introduced in C# 8 / .NET Core 3) allows async cleanup โ releasing resources that require I/O to close properly, such as database connections, network streams, or message broker consumers.
IDisposable.Dispose() is synchronous. If the cleanup involves async work (flushing a buffer to disk, sending a graceful disconnect message), calling Dispose() forces a sync-over-async bridge โ typically a .GetAwaiter().GetResult() inside Dispose() โ which can deadlock or block threads.
IAsyncDisposable.DisposeAsync() returns a ValueTask. Use await using to consume it correctly. In DI containers, ASP.NET Core's built-in container supports IAsyncDisposable โ registered services implementing it will be asynchronously disposed at scope end.
Best practice: implement both IDisposable and IAsyncDisposable on the same type when synchronous callers might use the type. Delegate Dispose() to DisposeAsync().AsTask().GetAwaiter().GetResult() only as a last resort, and document the risk.
Frequently Asked Questions
What is the best way to run multiple async tasks in parallel in C#?
Use Task.WhenAll to run independent tasks concurrently. Start all tasks first by calling the async methods without awaiting them, collect the resulting Task objects into an array, then pass the array to Task.WhenAll. This pattern launches all tasks simultaneously rather than sequentially. Do not use await inside a foreach loop if the iterations are independent โ that serialises them.
Is async void ever acceptable in ASP.NET Core?
No. In ASP.NET Core web APIs and background services, async void should never be used. There is no valid event-handler scenario in an API project. Use async Task for all methods. If you have a fire-and-forget use case (background work without the caller waiting for the result), use Task.Run, a hosted background service, or a dedicated queue (e.g., System.Threading.Channels) โ and still handle exceptions inside those constructs.
When should I use CancellationToken in ASP.NET Core controllers?
Always. Pass HttpContext.RequestAborted (exposed automatically as a parameter by ASP.NET Core if you add CancellationToken to the action method signature) to every database query, HTTP call, and I/O operation in the request pipeline. This ensures that if the client disconnects mid-request, the downstream work is cancelled rather than running to completion and wasting server resources.
What is the difference between Task.Delay and Thread.Sleep in async code?
Thread.Sleep blocks the calling thread for the specified duration โ the thread sits idle, consuming resources. Task.Delay is non-blocking: it returns a Task that completes after the delay without holding a thread. Inside an async method, use await Task.Delay(ms, cancellationToken). It accepts a CancellationToken so the delay can be cancelled early. Use Thread.Sleep only in synchronous contexts where blocking is acceptable and intentional.
How do I test async methods in xUnit or NUnit without deadlocks?
Both xUnit and NUnit fully support async Task test methods โ mark the test method as async Task (not async void) and the framework will correctly await the result and capture exceptions. Never use async void for test methods; the test runner cannot observe the result and exceptions will be swallowed. Use CancellationTokenSource with a timeout when testing methods that accept CancellationToken to ensure tests do not hang indefinitely.
What happens to unobserved task exceptions in modern .NET?
In .NET 4.5 and earlier, an unobserved TaskException would crash the process when the GC finalised the Task. In .NET 4.5+ (and all modern .NET versions), the default behaviour changed โ unobserved task exceptions are silently ignored. The TaskScheduler.UnobservedTaskException event still fires, which you can use for logging. The practical implication: fire-and-forget tasks that throw silently swallow exceptions. Always either observe the task result, propagate the exception, or register a handler on TaskScheduler.UnobservedTaskException to log and alert on failures.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
For further reading on related architectural decisions, see the ASP.NET Core DI Lifetimes: Singleton vs. Scoped vs. Transient โ Enterprise Decision Guide and the System.Threading.Channels in ASP.NET Core: Enterprise Decision Guide on Coding Droplets.
External references: Task asynchronous programming model โ Microsoft Docs | Async/await FAQ โ .NET Blog






