# C# Async/Await Interview Questions for Senior .NET Developers (2026)

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.

[![](https://newsletter.codingdroplets.com/images/aspnet-core-api-course-banner-1.jpg align="center")](https://aspnetcoreapi.codingdroplets.com/)

## Basic-Level Questions

Senior interviews test more than syntax knowledge - they probe whether you understand the runtime behaviour, the edge cases, and the patterns that hold up under production load. [Patreon](https://www.patreon.com/CodingDroplets) has in-depth implementations and annotated code that go beyond the Q&A format - showing these patterns applied inside a real ASP.NET Core API, with the context that makes them stick.

### 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 `.Result` or `.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()`, or `GetAwaiter().GetResult()` in library or shared code.
    
*   Use `ConfigureAwait(false)` in library code to break the context dependency.
    
*   Use `async` all 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 `await` directly without `Task.Run`.
    
*   **CPU-bound (heavy computation):** use `Task.Run` to 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.
    
*   `OperationCanceledException` is the expected exception on cancellation - do not swallow it without logging.
    
*   In ASP.NET Core, the `HttpContext.RequestAborted` token 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.
    
*   `ValueTask` and `ManualResetValueTaskSourceCore<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:

```plaintext
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.Run` wrapping of I/O-bound code
    
*   Poorly configured `MaxDegreeOfParallelism` in parallel loops
    
*   Deadlocking awaits that chain-consume threads
    

Diagnostic approach:

1.  Capture a thread dump (via `dotnet-dump` or Process Explorer) - look for large numbers of threads stuck in wait states.
    
2.  Use `EventPipe` + `dotnet-counters` to monitor `ThreadPool Queue Length` and `ThreadPool Thread Count` in real time.
    
3.  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.

* * *

For further reading on related architectural decisions, see the [ASP.NET Core DI Lifetimes: Singleton vs. Scoped vs. Transient - Enterprise Decision Guide](https://codingdroplets.com/aspnet-core-di-lifetimes-singleton-scoped-transient-enterprise-decision-guide) and the [System.Threading.Channels in ASP.NET Core: Enterprise Decision Guide](https://codingdroplets.com/system-threading-channels-aspnet-core-enterprise-decision-guide) on Coding Droplets.

External references: [Task asynchronous programming model - Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model) | [Async/await FAQ - .NET Blog](https://devblogs.microsoft.com/dotnet/async-in-4-5-worth-it/)

* * *

## About the Author

**Celin Daniel** is Co-founder of Coding Droplets with 13+ years of hands-on experience building, shipping, and operating .NET and ASP.NET Core systems in production. The guidance here comes from real projects and production incidents, not theory.

- Website: [codingdroplets.com](https://codingdroplets.com/)
- GitHub: [github.com/codingdroplets](http://github.com/codingdroplets/)
- YouTube: [youtube.com/@CodingDroplets](https://www.youtube.com/@CodingDroplets)
