If you only write ASP.NET Core APIs, you can skip ConfigureAwait(false). There is no UI thread to capture. Sprinkling it on every controller await does not make the API faster.
ConfigureAwait(false) is a library contract: “resume on any ThreadPool thread, do not post back to the caller’s UI.” You need it in NuGet / shared DLLs that might run inside WPF, WinForms, MAUI, or classic ASP.NET.
WPF deadlock without ConfigureAwait(false) in the library
UI thread: LoadHeaderAsync(...).Result ← blocked, waiting for the Task
│
library await ReadAsync captures UI context
│
ReadAsync completes → continuation needs the UI thread
│
UI thread is still in .Result → deadlock
New to this → stay here. Merging a PR → wrong vs right. On-call / interview → who has a context · .NET 8 options · if an interviewer asks.
Terms used here: SynchronizationContext = a rule for which thread continuations must run on (WPF dispatcher, Blazor circuit). Capture = after await, try to resume on that same context. ConfigureAwait(false) = do not capture; resume on a pool thread.
With ConfigureAwait(false) inside the library, the continuation can run on the pool. The UI thread is still blocked (still a bad handler) but it is not waiting on itself.
What await captures
The compiler builds a state machine. On an incomplete await, it stores locals and, by default, captures:
SynchronizationContext.Currentif it is not null- Otherwise the current
TaskScheduler
When the task completes, the continuation is posted back to that context.
public async Task<ClaimHeader> LoadHeaderAsync(Stream stream, CancellationToken ct)
{
var bytes = await stream.ReadAsync(buffer, ct).ConfigureAwait(false);
// Continuation is not required to run on the caller's UI thread
return ParseHeader(buffer.AsSpan(0, bytes));
}
In a NuGet package I do not know the host. That is why the false is there. Must means every incomplete await in the library, not the first one.
Wrong vs right
I would reject a PR that adds .ConfigureAwait(false) to every ASP.NET Core controller “for performance.” I would also reject a shared parser that omits it when WPF might call it.
// ASP.NET Core controller — skip false; no context to skip
var order = await _orders.GetByIdAsync(id, ct);
// Same helper in a library that WPF also references — use false
var bytes = await stream.ReadAsync(buffer, ct).ConfigureAwait(false);
// WPF — never do this, but people do
var header = parser.LoadHeaderAsync(stream, CancellationToken.None).Result;
Fix the WPF handler to async void (the one place async void is for) and await. The library just refused to join the deadlock. .Result on ASP.NET Core is starvation, not this deadlock.
Who has a SynchronizationContext
Comparison: which hosts have a SynchronizationContext and whether ConfigureAwait(false) matters.
| Host | Context? | ConfigureAwait(false) |
|---|---|---|
| ASP.NET Core (MVC, Minimal APIs, most workers) | No | Skip in app code |
| Console / generic host | No | Skip |
| WPF / WinForms / MAUI | Yes | Required in libraries; in app code only if you must resume off-UI |
| Blazor Server | Yes (circuit) | false leaves the circuit — do not touch component state after |
| Blazor WebAssembly | Yes (single thread) | Library still uses false; UI stays on the JS thread by design |
| Classic ASP.NET | Yes | Library false; do not .Result on the request thread |
| xUnit (modern) | Usually no | Optional |
ASP.NET Core removed the request SynchronizationContext on purpose. Continuations already run on the pool.
What it does not do
- Does not make
.Resultacceptable on an API - Does not speed up EF Core
- Does not replace CancellationToken
- Does not restore
HttpContextif you left the request
Blazor Server and HttpContext after false
Blazor Server has a circuit SynchronizationContext. That is the miss in the “Core has no context, skip false” slogan.
var rows = await _db.Fees.ToListAsync(ct).ConfigureAwait(false);
// Continuation is on the pool — not the circuit
grid.Items = rows; // wrong thread
var user = _http.HttpContext?.User; // do not assume this is still the circuit request
Comparison: what is safe after ConfigureAwait(false).
After ConfigureAwait(false) | Safe? |
|---|---|
| Circuit-bound UI / component fields | No — you left the circuit |
IHttpContextAccessor.HttpContext you did not capture | Copy claims / tenant id before the await |
| Plain DTO parse on the pool | Yes |
ASP.NET Core controllers still skip false — they have no circuit. UI yielding is Task.Yield.
.NET 8 ConfigureAwaitOptions
Task.ConfigureAwait(ConfigureAwaitOptions) is extra flags on Task / Task<T>, not yet on ValueTask in .NET 8.
ConfigureAwaitOptions.None— do not capture context (same idea asfalse)SuppressThrowing— await a faulted task without throwing (you must inspectStatus)ForceYielding— always yield, even if the task is already complete (cousin of Task.Yield)
Default await is unchanged: it still captures. App code on Core still does not need this for throughput.
Analyzers I actually turn on
- CA2007 /
ConfigureAwaitanalyzer on library projects - Off on the ASP.NET Core web project, or you will fight noise in every controller
One rule for MyCompany.Claims.Parsing.csproj. A different rule for MyCompany.Claims.Api.csproj.
Common mistakes
- Sprinkling
falseon controllers “for performance” - Using
falsethen touching UI-bound objects orHttpContextin the continuation - Forgetting
falseonawait foreachin a library - Believing Core “deadlocks like Framework” — it mostly starves instead
What this is not
Request-path async: async await in ASP.NET Core. UI message pump: Task.Yield. Topic map: async & threading hub.
If an interviewer asks
When is ConfigureAwait(false) still useful; does it help performance on Core; what SynchronizationContext is; what happens across an await on one request.
Strong answer: “Libraries yes. ASP.NET Core app code no. It is not a .Result amnesty.”