Introduction
Async and await in C# are foundational for modern backend systems, yet they remain one of the most misunderstood features in the language. Many developers treat async as a performance tool, expecting it to make code faster. In reality, async is about something far more subtle and far more important: efficient use of threads.
In high-load systems such as ASP.NET Core APIs, microservices, industrial integrations, and cloud-based backends, threads are a limited resource. Blocking threads unnecessarily leads to cascading failures. Systems that appear idle in terms of CPU usage may still suffer from severe latency, request queuing, and timeouts.
The root cause is often incorrect async usage.
This article explains:
- how async/await works under the hood
- what the compiler actually generates
- why await does not create threads
- how ThreadPool starvation occurs
- what “stolen threads” are
- how to write production-grade async code in C#
How async/await works internally
6
When you write an async method:
public async Task<OrderDto> GetOrderAsync(int id)
{
var order = await _repository.GetAsync(id);
return order;
}The C# compiler rewrites this method into a state machine. This is not a conceptual metaphor — it is actual generated code.
The method is transformed into a struct or class that contains:
- a state field (
int state) - an async method builder (
AsyncTaskMethodBuilder) - local variables stored as fields
- a
MoveNext()method controlling execution
Conceptually, execution looks like this:
state = -1
MoveNext()
{
if (state == -1)
{
var task = repository.GetAsync(id);
if (!task.IsCompleted)
{
state = 0;
register continuation;
return;
}
result = task.Result;
}
return result;
}Key takeaway:
👉 async methods are split into steps
👉 execution is paused and resumed
👉 state is stored between awaits
Await does NOT create a thread
8
One of the most dangerous misconceptions is that await creates a new thread.
It does not.
Consider this:
await httpClient.GetAsync(url);What really happens:
- HTTP request is initiated
- thread returns to ThreadPool
- OS handles network I/O
- when response arrives → continuation is scheduled
No thread is blocked while waiting.
This is why async scales.
I/O-bound vs CPU-bound work
7
I/O-bound (ideal for async)
await dbContext.Orders.ToListAsync();
await httpClient.GetAsync();
await fileStream.ReadAsync();✔ thread is released
✔ highly scalable
CPU-bound (not async)
await Task.Run(() => CalculateReport());✔ uses real thread
✔ consumes CPU
Important:
👉 wrapping I/O in Task.Run is a mistake
await Task.Run(() => db.Orders.ToList());This blocks a thread instead of freeing it.
Enterprise example – ASP.NET Core + CQRS
Controller
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> Get(int id, CancellationToken ct)
{
var result = await _mediator.Send(new GetOrderQuery(id), ct);
if (result == null)
return NotFound();
return Ok(result);
}Handler
public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
private readonly AppDbContext _db;
public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
{
return await _db.Orders
.Where(x => x.Id == request.Id)
.Select(x => new OrderDto
{
Id = x.Id,
Total = x.Total
})
.FirstOrDefaultAsync(ct);
}
}✔ no blocking
✔ async all the way
✔ production-ready
Parallel async (latency optimization)
var orderTask = _repo.GetAsync(id, ct);
var customerTask = _client.GetAsync(id, ct);
var stockTask = _stock.GetAsync(id, ct);
await Task.WhenAll(orderTask, customerTask, stockTask);This reduces latency by executing independent I/O in parallel.
The biggest trap: sync-over-async
var result = GetAsync().Result;or:
GetAsync().Wait();7
This causes:
- thread blocking
- lost scalability
- starvation under load
ThreadPool starvation explained
ThreadPool is a shared pool of worker threads used by:
- ASP.NET requests
- async continuations
- background services
When threads are blocked:
- new work cannot execute
- continuations cannot run
- queue grows
- latency increases dramatically
Critical insight:
👉 CPU may be low
👉 system still appears “down”
“Stolen threads” problem
await _repo.GetAsync(id);
Thread.Sleep(3000);or:
await _repo.GetAsync(id);
DoHeavyCpuWork();Continuation runs on ThreadPool thread.
If you block or do heavy work:
👉 you steal the thread
👉 async becomes useless
Deadlocks in async code
5
var result = SomeAsync().Result;Flow:
- thread blocks
- async captures context
- continuation waits for thread
- deadlock
Async with HttpClient
public async Task<CustomerDto> GetCustomerAsync(int id, CancellationToken ct)
{
var response = await _http.GetAsync($"/api/customers/{id}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<CustomerDto>(cancellationToken: ct);
}Async with RabbitMQ
public async Task HandleAsync(Message msg, CancellationToken ct)
{
await ProcessAsync(msg, ct);
_channel.BasicAck(msg.DeliveryTag, false);
}Never acknowledge before async work finishes.
Background services
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessQueue(stoppingToken);
await Task.Delay(500, stoppingToken);
}
}CancellationToken
await dbContext.Orders.ToListAsync(ct);Without it:
- wasted work
- poor scalability
ConfigureAwait(false)
await SomeAsync().ConfigureAwait(false);Useful in libraries to avoid context capture.
Async pitfalls checklist
- ❌ .Result / .Wait
- ❌ Task.Run for I/O
- ❌ async void
- ❌ fire-and-forget
- ❌ missing cancellation
- ❌ blocking calls
Conclusion
Async in C# is not about speed.
It is about not blocking threads.
If used incorrectly, it creates:
- hidden latency
- deadlocks
- ThreadPool starvation
If used correctly, it enables:
- high scalability
- better resource usage
- stable systems under load
Source
https://github.com/rafalkukuczka/AsyncAwaitDeepDiveDemo
https://github.com/rafalkukuczka/AsyncAwaitRulesDemo
https://github.com/rafalkukuczka/AsyncAwait
References
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming
https://www.linkedin.com/posts/aramt87_async-bugs-dont-show-up-in-testing-they-activity-7459914623773839361-3iEE?utm_source=share&utm_medium=member_desktop&rcm=ACoAAABO4P4B1TQKtdVStvHjxgI8amSeFiSleBg (Credits to Aram Tschekrekjian)
More Info
We implement this in real production systems.
If you need help → contact us