How does async/await work under the hood?
Manav Pandya
When you write something like:
public async Task<int> GetDataAsync(){ var result = await CallExternalApiAsync(); return result + 1;}
public async Task<int> GetDataAsync()
{
var result = await CallExternalApiAsync();
return result + 1;
}
The C# compiler does not just make it “multithreaded.” Instead:
The compiler rewrites your method into a state machine (like how iterators work with yield return).
Each await is a checkpoint where the method can pause and resume later.
The method doesn’t block the thread. Instead, it returns a Task immediately.
await checks if the awaited task is already completed:
If yes — continue execution synchronously.
If no — register a continuation callback (what to do when the task finishes) and return control to the caller.
The continuation is scheduled to run on a SynchronizationContext (UI thread in WPF/WinForms, request context in ASP.NET) or on the ThreadPool if no context is captured.
This:
var data = await CallExternalApiAsync();Console.WriteLine(data);
var data = await CallExternalApiAsync();
Console.WriteLine(data);
Is transformed roughly into:
CallExternalApiAsync() .GetAwaiter() .OnCompleted(() => { var data = task.Result; // resume execution Console.WriteLine(data); });
CallExternalApiAsync()
.GetAwaiter()
.OnCompleted(() =>
var data = task.Result; // resume execution
});
So instead of blocking, it sets up a callback to resume later.
Caller calls GetDataAsync() it runs until the first await.
GetDataAsync()
At await, it:
Checks if task is done → if not, attach a continuation.
Returns a Task to the caller.
When CallExternalApiAsync() finishes:
It signals its completion.
The continuation runs → method resumes from the await.
Eventually, the outer Task<int> completes with the result.
Task<int>
async/await does not create new threads.
async/await
If work is I/O bound (HTTP, file, DB call), the thread is released until the result is ready.
If work is CPU-bound, you’d use Task.Run to move it to a background thread.
Task.Run
There’s no SynchronizationContext like classic ASP.NET.
Continuations just run on the ThreadPool.
This avoids deadlocks that used to happen in old ASP.NET when mixing .Result and await.
.Result
await
Key takeaway for interviews:
async/await = compiler-generated state machine.
await = non-blocking checkpoint that sets up a continuation.
Execution resumes when the awaited task completes.
It’s about asynchronous programming, not about creating threads.