Async Programming and Multithreading Interview Questions for .NET Full Stack
25 Async Programming and Multithreading questions with answers for .net full stack roles — 4 easy, 13 medium, 8 hard.
About Async Programming and Multithreading
async and await, Tasks, cancellation, thread safety and parallel processing in .NET. Interviewers ask about deadlocks, thread pool starvation, ConfigureAwait, ValueTask, locks, SemaphoreSlim, channels and background services, because correct asynchronous code is what lets ASP.NET Core APIs handle thousands of concurrent users without wasting server resources.
Sheet progressStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
async marks a method that can contain await, and the compiler turns it into a state machine. await pauses the method until a Task finishes without blocking the thread; the thread returns to the pool to do other work and the method resumes later, often on another thread. It is ideal for I/O such as database calls, HTTP requests and file reads. An async method returns Task, Task<T> or ValueTask.
02
A dashboard API calls three independent services one after another, each taking about 1 second. How can you make it faster?
Because the calls do not depend on each other, start all three tasks first without awaiting, then await Task.WhenAll. Total time becomes roughly the slowest call, about 1 second instead of 3. Add a timeout and handle failure of one service gracefully, for example showing partial data. If the same data is requested often, also add caching so most requests skip the calls entirely.
03
What is the difference between concurrency and parallelism?
Concurrency means dealing with many tasks at once by interleaving them, for example a web server handling thousands of requests that mostly wait on I/O. Parallelism means doing many computations at the same instant on multiple cores, such as processing an image in four chunks. async/await gives concurrency; Parallel.For and PLINQ give parallelism. A program can be concurrent on a single core but parallel only with multiple cores.
04
What is the difference between CPU-bound and I/O-bound work, and how do you handle each in .NET?
CPU-bound work keeps the processor busy, like encryption, image processing or heavy calculations; speed it up with parallelism using Parallel.For, PLINQ or Task.Run in desktop apps. I/O-bound work spends most time waiting on databases, files or networks; handle it with async/await so threads are released while waiting. Choosing wrongly wastes resources, such as using Parallel.ForEach for HTTP calls or Task.Run around database queries.
05
What is the difference between a Task and a Thread?
A Thread is an OS-level execution unit with its own stack, costly to create. A Task is a higher-level promise of work that may or may not use a thread; I/O tasks often use no thread while waiting, and CPU tasks run on pooled threads. Tasks support return values, continuations, cancellation, exceptions and composition with WhenAll. In modern .NET you almost always use Tasks rather than creating threads manually.
06
Why does using async in ASP.NET Core controllers improve scalability?
Each request needs a thread from the thread pool. If a controller blocks while waiting for SQL Server or an external API, that thread sits idle and cannot serve other users, so under load the pool runs out and requests queue. With async I/O, the thread is released during the wait and reused, so the same server handles many more concurrent requests. It does not make a single request faster.
An async void method cannot be awaited, so the caller does not know when it finishes. Exceptions thrown inside it cannot be caught by the caller; they go to the synchronization context and can crash the process. It is also hard to unit test. Use async Task instead. The only accepted use of async void is for event handlers, such as button click handlers in desktop apps, where the signature is fixed.
// run three independent calls in parallel
var userTask = userApi.GetAsync(id);
var ordersTask = orderApi.GetAsync(id);
var walletTask = walletApi.GetAsync(id);
await Task.WhenAll(userTask, ordersTask, walletTask);
var dashboard = new Dashboard(userTask.Result, ordersTask.Result, walletTask.Result);
// timeout using WhenAny
var work = SlowCallAsync();
if (await Task.WhenAny(work, Task.Delay(2000)) != work)
throw new TimeoutException();
WhenAll completes when all finish; WhenAny returns the first task to finish.
09
How do you use CancellationToken in an ASP.NET Core API?
[HttpGet("report")]
public async Task<IActionResult> Report(CancellationToken ct)
{
var rows = await _db.Sales
.Where(s => s.Year == 2026)
.ToListAsync(ct);
await _http.PostAsJsonAsync("/audit", rows.Count, ct);
return Ok(rows);
}
ASP.NET Core binds the token to HttpContext.RequestAborted, so if the user closes the browser the token is cancelled and EF Core and HttpClient stop work, saving database and CPU time.
lock ensures that only one thread at a time executes a block of code for a given lock object, preventing race conditions on shared data. It compiles to Monitor.Enter and Monitor.Exit inside try-finally. Lock on a private readonly object, never on this or a string. Since .NET 9 there is a dedicated System.Threading.Lock type that is faster. You cannot await inside a lock; use SemaphoreSlim for async code.
11
Demonstrate a race condition on a counter and fix it.
int count = 0;
Parallel.For(0, 100_000, _ => count++);
Console.WriteLine(count); // often less than 100000
int safe = 0;
Parallel.For(0, 100_000, _ => Interlocked.Increment(ref safe));
Console.WriteLine(safe); // always 100000
count++ is a read, add and write, so two threads can read the same value and lose an update. Interlocked performs the increment atomically without a full lock.
12
You must resize 10,000 images and also call an external API for 10,000 records. Which approach do you use for each task?
Parallel.ForEach is for CPU-bound work, such as resizing thousands of images, where splitting work across cores speeds things up. async/await is for I/O-bound work, where you mostly wait on networks or disks. Using Parallel.ForEach for HTTP calls wastes threads while they wait. For many concurrent I/O operations with a limit, use Parallel.ForEachAsync with MaxDegreeOfParallelism, available since .NET 6.
public async IAsyncEnumerable<Order> GetOrdersAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var o in _db.Orders.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct))
yield return o;
}
// consumer
await foreach (var order in service.GetOrdersAsync())
Console.WriteLine(order.Id);
Items are produced and consumed one at a time asynchronously. ASP.NET Core can stream an IAsyncEnumerable result as JSON without buffering the whole list.
14
Write a BackgroundService that cleans expired OTPs every 5 minutes.
public class OtpCleanupService(IServiceScopeFactory scopes) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
using var scope = scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Otps.Where(o => o.ExpiresAt < DateTime.UtcNow).ExecuteDeleteAsync(ct);
}
}
}
// builder.Services.AddHostedService<OtpCleanupService>();
A scope is created because DbContext is scoped while hosted services are singletons.
15
A developer wraps every database call in Task.Run inside ASP.NET Core controllers to make them async. Is this a good idea?
No. Task.Run moves work to another thread pool thread, but the request still waits for it, so you use two threads instead of one and gain nothing. The database call itself is still blocking. The correct approach is to use true async APIs like ToListAsync and SaveChangesAsync, which release the thread during I/O. Task.Run makes sense mainly in desktop apps to keep the UI thread free.
16
Users trigger a large Excel report that takes 3 minutes, and the HTTP request times out. How would you redesign it?
Make it asynchronous at the workflow level. The API validates the request, saves a job record and returns 202 Accepted with a job ID. A background worker, using a queue such as a Channel, Hangfire or a message broker, generates the file and stores it in blob storage. The UI polls a status endpoint or receives a SignalR notification, then downloads the file. This avoids timeouts and survives retries.
17
Tell me about a production bug you had to debug under time pressure.
Last year (Situation) our order API started timing out during a sale. My task was to restore service fast. I checked dashboards, saw thread count climbing with low CPU, and took a dump that showed threads blocked on a .Result call added in a recent release. Action: I hotfixed it to proper async and rolled out within an hour. Result: latency returned to normal, and I added an analyser rule to block sync-over-async.
18
A legacy ASP.NET MVC app on .NET Framework hangs when a controller calls GetDataAsync().Result. Why, and how do you fix it?
This is the classic sync-over-async deadlock. .Result blocks the request thread, which holds the ASP.NET synchronization context. When the awaited task completes, its continuation tries to resume on that same context, which is blocked, so neither can proceed. The real fix is async all the way: make the action async and await the call. Where that is impossible, ConfigureAwait(false) inside the library avoids capturing the context.
19
What does ConfigureAwait(false) do and is it needed in ASP.NET Core?
By default await captures the current SynchronizationContext and resumes on it. ConfigureAwait(false) tells the runtime it can resume on any thread pool thread, which avoids deadlocks in UI and old ASP.NET apps and slightly reduces overhead. ASP.NET Core has no SynchronizationContext, so it is not required in application code there. It is still recommended in general-purpose libraries that might be used by WPF or WinForms apps.
20
Profiling shows a hot cache-lookup method returning Task<T> allocates millions of Task objects. Would ValueTask help?
ValueTask<T> is a struct that avoids allocating a Task when a method usually completes synchronously, for example when a value is served from an in-memory cache most of the time. In very hot paths this reduces GC pressure. The rules are stricter: await a ValueTask only once, do not await it concurrently, and do not call .Result before it completes. For normal application code, Task is simpler and recommended.
21
Limit concurrent calls to a third-party API to 5 at a time using SemaphoreSlim.
This pattern points to thread pool starvation. Some code is blocking threads with .Result, .Wait(), Thread.Sleep or synchronous I/O, so pool threads are stuck waiting and the pool injects new threads slowly. Confirm with dotnet-counters, watching thread pool queue length and thread count, and a dump showing many threads blocked in Wait. Fix by making the call chain fully async; raising minimum threads is only a temporary patch.
23
What are System.Threading.Channels and when would you use them?
Channels provide fast, thread-safe, async producer-consumer queues. A producer writes with await writer.WriteAsync and a consumer reads with await foreach over reader.ReadAllAsync. Bounded channels apply back-pressure by making producers wait when full. They are useful inside one process, for example an API that accepts uploads quickly and hands them to a background worker. For durable or cross-service queues use a broker like RabbitMQ or Azure Service Bus.
24
How are exceptions handled in async code, and what is AggregateException?
When you await a faulted task, the original exception is rethrown, so normal try-catch works. If you block with .Wait() or .Result, the exception is wrapped in AggregateException. With Task.WhenAll, await throws only the first exception, but the returned task's Exception property holds all of them in an AggregateException. Unobserved exceptions from fire-and-forget tasks are silently lost, so always await or log them.
A SynchronizationContext represents where continuations should run. WPF and WinForms have one that marshals work back to the UI thread, so after await you can update controls safely. Classic ASP.NET had one tied to the request. ASP.NET Core and console apps have none, so continuations run on any thread pool thread. Understanding it explains why .Result deadlocks in UI apps but often not in ASP.NET Core.
Frequently asked questions
How important is Async Programming and Multithreading in .NET Full Stack interviews?
async and await, Tasks, cancellation, thread safety and parallel processing in .NET. Interviewers ask about deadlocks, thread pool starvation, ConfigureAwait, ValueTask, locks, SemaphoreSlim, channels and background services, because correct asynchronous code is what lets ASP.NET Core APIs handle thousands of concurrent users without wasting server resources.
How should I use the Async Programming and Multithreading sheet?
Go top to bottom — easy to hard. Answer before opening each answer, tick what you solved and save misses to Last-Minute Revision.