Todo desenvolvedor C# já viu ConfigureAwait(false) espalhado por código de bibliotecas e se perguntou se deve copiar esse padrão. A resposta curta depende inteiramente do tipo de código que você está escrevendo e do ambiente de execução que o hospeda.
O que é SynchronizationContext?
SynchronizationContext é uma abstração que permite que o runtime do .NET saiba onde publicar uma continuação após a conclusão de um await. Pense nele como um agendador que diz "quando esta operação assíncrona terminar, retome a execução aqui."
Diferentes modelos de aplicação instalam contextos diferentes:
| Tipo de aplicação | SynchronizationContext presente? | O que faz |
|---|---|---|
| WinForms | Sim (WindowsFormsSynchronizationContext) | Direciona continuações de volta à thread de UI |
| WPF | Sim (DispatcherSynchronizationContext) | Idem — direciona à thread do Dispatcher |
| ASP.NET (clássico, .NET Framework) | Sim (AspNetSynchronizationContext) | Garante que apenas uma thread execute por requisição |
| ASP.NET Core | Não | Sem contexto; threads do pool são usadas diretamente |
| Aplicação console (.NET 5+) | Não | Sem contexto; o thread pool é usado |
| Blazor WebAssembly | Sim | Single-threaded; contexto direciona de volta à thread do JS |
| xUnit / NUnit / MSTest | Varia por versão | Alguns instalam contexto para sincronização em testes |
A presença ou ausência de um SynchronizationContext é o fato-chave que orienta cada decisão neste artigo.
ConfigureAwait(true) — O Comportamento Padrão
Quando você escreve um await simples, o C# o compila como await someTask.ConfigureAwait(true). O argumento true significa:
- Antes de suspender, capturar o
SynchronizationContextatual (ouTaskSchedulerse não houver contexto). - Quando a tarefa aguardada concluir, publicar a continuação de volta para esse contexto capturado.
Isso é o que torna o código de UI seguro — você pode fazer await de uma chamada de rede e então tocar em um TextBox na linha seguinte sem um Dispatcher.Invoke explícito:
// Code-behind do WPF — ConfigureAwait(true) é o padrão
private async void Button_Click(object sender, RoutedEventArgs e)
{
string result = await FetchDataAsync(); // suspende aqui
// Retoma na thread de UI — seguro para tocar controles
myLabel.Content = result;
}O custo: após a tarefa concluir em uma thread do pool, o runtime precisa publicar a continuação de volta ao contexto capturado (por exemplo, o loop de mensagens da UI). Essa ida e volta tem um custo, e em frameworks como o ASP.NET clássico pode causar deadlocks.
ConfigureAwait(false) — Ignorando a Recaptura do Contexto
ConfigureAwait(false) diz ao awaiter: "Não preciso retomar no contexto original. Retome em qualquer thread disponível (normalmente uma thread do pool)."
string result = await FetchDataAsync().ConfigureAwait(false);
// Retoma em uma thread do pool — NÃO tocar controles de UI aquiInternamente, a máquina de estados gerada pelo compilador verifica o sinalizador ContinueOnCapturedContext do awaiter. Quando é false, a continuação é agendada diretamente no thread pool (via ThreadPool.QueueUserWorkItem ou similar), em vez de ser publicada no SynchronizationContext capturado.
Comparação Passo a Passo
// --- Fluxo com ConfigureAwait(true) ---
// Thread: thread de UI (SynchronizationContext = DispatcherSynchronizationContext)
await Task.Delay(100);
// 1. DispatcherSynchronizationContext é capturado
// 2. Task.Delay conclui na thread do pool T2
// 3. T2 publica a continuação na fila do Dispatcher
// 4. Thread de UI a recolhe da fila
// Thread: thread de UI novamente ✓
// --- Fluxo com ConfigureAwait(false) ---
// Thread: thread de UI (SynchronizationContext = DispatcherSynchronizationContext)
await Task.Delay(100).ConfigureAwait(false);
// 1. SynchronizationContext intencionalmente NÃO é capturado
// 2. Task.Delay conclui na thread do pool T2
// 3. Continuação executa diretamente em T2 (sem ida e volta ao Dispatcher)
// Thread: thread do pool T2Medido: Contando os Posts
Essa descrição do fluxo é verificável, porque "publicar a continuação de volta" é uma
chamada a SynchronizationContext.Post. samples/configureawait-false-csharp
instala um contexto de thread única estilo UI com um contador em Post e executa o mesmo
método das duas formas — números determinísticos, idênticos em qualquer máquina, ao
contrário de tempos em nanossegundos:
| Cenário (na thread do contexto) | Awaits | Posts contados | Também verificado |
|---|---|---|---|
| Awaits simples | 5 | 5 | o contexto sobrevive; ainda na thread estilo UI |
Todos com ConfigureAwait(false) | 5 | 0 | Current é null após o primeiro await; fora da thread de UI |
ConfigureAwait(false) só no primeiro await | 1 + 4 simples | 0 | a regra de propagação abaixo |
| Sem contexto (thread de console) | 2 | n/a | as duas formas se comportam idêntico — o caso do ASP.NET Core |
Um await, um Post; ConfigureAwait(false) pula exatamente isso. Todo o resto deste
artigo deriva desses números.

O Deadlock Clássico — Por Que ConfigureAwait(false) Importa
A razão mais importante para conhecer essa API é a prevenção de deadlocks em padrões de bloqueio sobre código assíncrono. Considere o ASP.NET clássico (.NET Framework), onde um SynchronizationContext limita a concorrência a uma thread por requisição:
// Método de biblioteca — NÃO usa ConfigureAwait(false)
public async Task<string> GetDataAsync()
{
await Task.Delay(500); // captura AspNetSynchronizationContext
return "hello";
}
// Action do controlador — bloqueia de forma síncrona (má prática, mas acontece)
public ActionResult Index()
{
// .Result bloqueia a thread da requisição enquanto segura o bloqueio do contexto
string data = GetDataAsync().Result; // DEADLOCK
return Content(data);
}Por que ocorre o deadlock:
Index()chamaGetDataAsync()e bloqueia a thread da requisição com.Result.- A thread da requisição retém o
AspNetSynchronizationContext. Task.Delayconclui e tenta publicar a continuação de volta para esse contexto.- O contexto está bloqueado (a thread da requisição está bloqueada em
.Result). - A continuação aguarda o contexto. A thread bloqueada aguarda a continuação. Deadlock.
Solução com ConfigureAwait(false):
public async Task<string> GetDataAsync()
{
// NÃO tenta retornar ao AspNetSynchronizationContext
await Task.Delay(500).ConfigureAwait(false);
return "hello"; // executa em uma thread do pool — sem contexto necessário
}Agora, quando Task.Delay conclui, a continuação é executada em uma thread do pool em vez de tentar re-entrar no contexto retido. O deadlock é quebrado.
O deadlock só ocorre quando alguém chama .Result, .Wait(), ou GetAwaiter().GetResult() em um método assíncrono que usa o ConfigureAwait(true) padrão. A solução real é tornar toda a cadeia de chamadas assíncrona. ConfigureAwait(false) é uma rede de segurança, não uma licença para bloquear.
Deadlock no WinForms / WPF — O Mesmo Padrão
// WPF: handler de clique que chama .Result (nunca faça isso em código real)
private void BadButton_Click(object sender, RoutedEventArgs e)
{
// Thread de UI bloqueia, retém DispatcherSynchronizationContext
string result = SomeLibraryMethod().Result; // deadlock se a biblioteca usa ConfigureAwait(true)
myLabel.Content = result;
}
// Biblioteca — correto: usa ConfigureAwait(false) em todo lugar
public async Task<string> SomeLibraryMethod()
{
var data = await HttpClient.GetStringAsync("https://example.com")
.ConfigureAwait(false); // não tentará publicar de volta ao Dispatcher
return data.ToUpper();
}Quando ConfigureAwait(false) É Irrelevante
ASP.NET Core
O ASP.NET Core deliberadamente não tem SynchronizationContext. Quando não há contexto para capturar, ConfigureAwait(true) e ConfigureAwait(false) se comportam de forma idêntica — ambos retomam em uma thread do pool.
// Controlador do ASP.NET Core — ConfigureAwait(false) não tem efeito aqui
[HttpGet("data")]
public async Task<IActionResult> GetData()
{
// Comportamento idêntico com ou sem ConfigureAwait(false)
var result = await _service.FetchAsync();
return Ok(result);
}Aplicações Console (.NET 5+)
Aplicações console também não têm SynchronizationContext. Qualquer await já retoma no thread pool. Adicionar ConfigureAwait(false) é uma operação nula.
static async Task Main(string[] args)
{
// Sem SynchronizationContext — ConfigureAwait(false) é redundante
var data = await File.ReadAllTextAsync("input.txt").ConfigureAwait(false);
Console.WriteLine(data);
}Por Que Código de Biblioteca Deve Sempre Usar ConfigureAwait(false)
O autor de uma biblioteca não pode saber quem chamará seu código. O chamador pode ser:
- Uma aplicação WPF com
DispatcherSynchronizationContext - Uma aplicação ASP.NET clássica com
AspNetSynchronizationContext - Uma aplicação Blazor com seu próprio contexto
- Um executor de testes unitários que instala um contexto personalizado
Se a biblioteca usa ConfigureAwait(true) (o padrão) e o chamador bloqueia em .Result, o cenário de deadlock se torna possível. Se a biblioteca usa ConfigureAwait(false), ela opta por sair completamente do contexto do chamador e não pode contribuir para o deadlock.
// Bom código de biblioteca — agnóstico ao contexto em todo lugar
public class DataClient
{
private readonly HttpClient _http;
public DataClient(HttpClient http) => _http = http;
public async Task<UserDto> GetUserAsync(int id)
{
// ConfigureAwait(false) em cada await — a biblioteca não possui o contexto
var response = await _http.GetAsync($"/users/{id}").ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonSerializer.Deserialize<UserDto>(json)!;
}
public async Task<IReadOnlyList<OrderDto>> GetOrdersAsync(int userId)
{
var response = await _http.GetAsync($"/users/{userId}/orders").ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var orders = await JsonSerializer.DeserializeAsync<List<OrderDto>>(stream).ConfigureAwait(false);
return orders ?? [];
}
}A regra geral: se você está escrevendo um pacote NuGet ou biblioteca compartilhada, adicione ConfigureAwait(false) a cada await. Se está escrevendo código de aplicação voltado ao ASP.NET Core ou aplicação console, é opcional e adiciona ruído visual sem nenhum benefício.
A Regra de Propagação de Contexto
Uma vez que você usa ConfigureAwait(false) em um único await dentro de um método, todos os awaits subsequentes nesse mesmo método também podem executar sem o contexto, independentemente de sua própria configuração de ConfigureAwait. Isso ocorre porque SynchronizationContext.Current se torna null na thread do pool onde a execução continua.
public async Task ExampleAsync()
{
// Ainda no contexto original
await FirstOperation().ConfigureAwait(false);
// Agora na thread do pool — SynchronizationContext.Current é null
// Este ConfigureAwait(true) é efetivamente igual a ConfigureAwait(false)
// porque não há contexto para retornar
await SecondOperation().ConfigureAwait(true); // redundante — sem contexto presente
await ThirdOperation(); // também está correto — sem contexto para capturar
}Isso não significa que você deva ser inconsistente. Escreva ConfigureAwait(false) em cada await no código de biblioteca para ser explícito e evitar confusão.
O sample verifica essa regra por contagem: um ConfigureAwait(false) seguido de quatro
awaits simples produziu zero Posts — os awaits simples já não tinham contexto a capturar.
Analisadores Roslyn
Adicionar manualmente ConfigureAwait(false) a cada await é tedioso e sujeito a erros. Vários analisadores Roslyn aplicam isso automaticamente.
Microsoft.VisualStudio.Threading.Analyzers
Instale via NuGet:
dotnet add package Microsoft.VisualStudio.Threading.AnalyzersEste pacote fornece a regra VSTHRD111 (UseConfigureAwait) que avisa sobre qualquer await que não tenha ConfigureAwait:
// Aviso VSTHRD111: Use .ConfigureAwait(false) ao aguardar uma tarefa
var result = await SomeTaskAsync(); // ⚠ VSTHRD111Configure no .editorconfig para tratá-lo como erro em projetos de biblioteca:
[*.cs]
dotnet_diagnostic.VSTHRD111.severity = errorRoslynator
dotnet add package Roslynator.AnalyzersA regra RCS1090 (UseConfigureAwaitFalse) fornece aplicação similar.
Abordagem com .editorconfig
Você também pode configurar o analisador CA2007 integrado (disponível nos analisadores do SDK do .NET):
[*.cs]
# CA2007: Considere chamar ConfigureAwait na tarefa aguardada
dotnet_diagnostic.CA2007.severity = warningNota: CA2007 só é acionado ao compilar com <EnableNETAnalyzers>true</EnableNETAnalyzers>, que é o padrão para novos projetos no estilo SDK voltados ao .NET 5+.
.NET 8: ConfigureAwaitOptions
Primeiro, desfazendo um mito que este próprio artigo costumava repetir: não existe um
padrão de ConfigureAwait(false) em nível de projeto no .NET — nem propriedade de
MSBuild, nem RuntimeHostConfigurationOption, nem atributo em nível de assembly. Uma
versão anterior deste artigo descrevia tal mecanismo; ele não existe — o
ConfigureAwait FAQ de Stephen
Toub explica diretamente por que a equipe recusou repetidamente adicionar um interruptor
global. A única alavanca em nível de projeto continua sendo a aplicação com analisadores
(abaixo).
O que o .NET 8 realmente adicionou é uma sobrecarga: ConfigureAwait(ConfigureAwaitOptions),
um enum de flags que generaliza o booleano:
// ConfigureAwaitOptions.None == ConfigureAwait(false)
await task.ConfigureAwait(ConfigureAwaitOptions.None);
// ConfigureAwaitOptions.ContinueOnCapturedContext == ConfigureAwait(true)
await task.ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext);
// Novo: conclui o await mesmo que a task tenha falhado ou sido cancelada — não
// relança a exceção. Só é válido em Task não genérica (uma Task<T> não teria
// resultado para te devolver).
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// Novo: sempre cede o controle, mesmo que a task já tenha concluído — o await nunca
// continua sincronamente. Útil para forçar justiça ou sair de um caminho com lock.
await task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
// São flags, então se combinam:
await task.ConfigureAwait(
ConfigureAwaitOptions.SuppressThrowing | ConfigureAwaitOptions.ForceYielding);A sobrecarga existe em Task e Task<TResult> (não em ValueTask no .NET 8), e
SuppressThrowing em uma Task<TResult> lança ArgumentOutOfRangeException na chamada
ao ConfigureAwait — o uso incorreto falha rápido em vez de devolver silenciosamente um
resultado padrão.
A Verdadeira Solução em Nível de Projeto: Aplicar com Analisadores
Como não existe um padrão, a abordagem prática para todo o repositório é transformar a
regra do analisador em erro no Directory.Build.props:
<!-- Directory.Build.props — aplica a todos os projetos na árvore de diretório -->
<Project>
<PropertyGroup>
<!-- Tratar ConfigureAwait faltante como erro de compilação em todo o repositório -->
<WarningsAsErrors>$(WarningsAsErrors);CA2007</WarningsAsErrors>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
</Project>Equívocos Comuns
Equívoco 1: ConfigureAwait(false) Melhora a Performance
A medição acima dá forma precisa a isso: o que ConfigureAwait(false) economiza é um
Post por await — e no ASP.NET Core ou em uma app de console, zero, porque não há
contexto para o qual publicar. Um post de contexto é barato; a razão para usar
ConfigureAwait(false) é blindar código de biblioteca contra deadlocks, não velocidade.
Não o adicione ao código de aplicação por performance — o ruído que adiciona supera
qualquer ganho teórico.
// NÃO faça isso por "performance" no ASP.NET Core — adiciona ruído sem benefício
public async Task<string> GetDataAsync()
{
return await _cache.GetAsync("key").ConfigureAwait(false); // inútil no ASP.NET Core
}Equívoco 2: ConfigureAwait(false) Afeta o Tratamento de Erros
A propagação de exceções funciona de forma idêntica independentemente do ConfigureAwait. Exceções lançadas dentro de uma tarefa aguardada ainda são capturadas e relançadas no ponto await, independentemente de qual thread executa essa continuação. (A última verificação do sample confirma isso: um método que lança depois de um await com ConfigureAwait(false) é capturado por um try/catch comum no ponto de chamada. A única exceção à regra no .NET 8 é opcional: ConfigureAwaitOptions.SuppressThrowing, coberta acima.)
public async Task DemonstrateExceptionAsync()
{
try
{
// Exceção lançada dentro de FetchAsync é relançada aqui,
// independentemente de ConfigureAwait
await FetchAsync().ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
// Ainda é capturada — ConfigureAwait não afeta o fluxo de exceções
_logger.LogError(ex, "Fetch failed");
throw;
}
}Equívoco 3: ConfigureAwait(false) Torna o Código Thread-Safe
ConfigureAwait(false) apenas muda em qual thread a continuação é executada. Não tem efeito sobre thread safety, estado compartilhado ou condições de corrida. Você ainda precisa de primitivas de sincronização adequadas ao acessar estado mutável compartilhado.
Equívoco 4: Você Precisa de ConfigureAwait(false) em Métodos async void
Métodos async void são handlers de eventos e cenários de disparar-e-esquecer. Eles ainda capturam o SynchronizationContext e ainda se beneficiam de ConfigureAwait(false) se chamam código de biblioteca que pode estar bloqueado. Mas as mesmas regras se aplicam — você só precisa dele para prevenir deadlocks ou evitar trocas de contexto desnecessárias em frameworks que têm um contexto.
Padrões Práticos
Padrão 1: Biblioteca Wrapper de HttpClient
public class WeatherApiClient
{
private readonly HttpClient _http;
private readonly ILogger<WeatherApiClient> _logger;
public WeatherApiClient(HttpClient http, ILogger<WeatherApiClient> logger)
{
_http = http;
_logger = logger;
}
public async Task<WeatherForecast?> GetForecastAsync(
string city,
CancellationToken cancellationToken = default)
{
// Código de biblioteca: ConfigureAwait(false) em cada await
var url = $"/api/weather?city={Uri.EscapeDataString(city)}";
using var response = await _http
.GetAsync(url, cancellationToken)
.ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Weather API returned {StatusCode}", response.StatusCode);
return null;
}
await using var stream = await response.Content
.ReadAsStreamAsync(cancellationToken)
.ConfigureAwait(false);
return await JsonSerializer
.DeserializeAsync<WeatherForecast>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
}Padrão 2: ViewModel do WPF — Onde NÃO Usar ConfigureAwait(false)
public class MainViewModel : INotifyPropertyChanged
{
private string _status = string.Empty;
public string Status
{
get => _status;
set { _status = value; OnPropertyChanged(); }
}
public async Task LoadDataAsync()
{
// Await simples, deliberadamente: este método define estado vinculado à UI
// depois, então precisa voltar à thread de UI. Os ConfigureAwait(false) vão
// DENTRO de _apiClient (código de biblioteca) — a fronteira entre "I/O sem
// contexto" e "atualização de UI com contexto" é a fronteira da API,
// não uma linha deste método.
var data = await _apiClient.GetForecastAsync("London");
// Retomado na thread de UI — seguro tocar estado vinculado
Status = data?.Summary ?? "Sem dados";
}
}Um padrão que você encontrará por aí (e em uma versão anterior deste artigo): chamar
TaskScheduler.FromCurrentSynchronizationContext() depois de um
await ... ConfigureAwait(false) para "voltar" à UI. Isso lança
InvalidOperationException — depois de ConfigureAwait(false) não há
SynchronizationContext atual para capturar (o demo 2 do sample verifica exatamente
isso: Current é null). Se um método precisa da thread de UI depois dos seus awaits, use
await simples nesse método e empurre ConfigureAwait(false) para a biblioteca que ele chama.
Padrão 3: Repositório do Entity Framework Core
public class UserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context) => _context = context;
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
{
// EF Core suporta ConfigureAwait(false) — seguro em código de biblioteca
return await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == id, ct)
.ConfigureAwait(false);
}
public async Task<int> CreateUserAsync(User user, CancellationToken ct = default)
{
_context.Users.Add(user);
await _context.SaveChangesAsync(ct).ConfigureAwait(false);
return user.Id;
}
}Tabela de Decisão
| Cenário | Usar ConfigureAwait(false)? | Motivo |
|---|---|---|
| Pacote NuGet / biblioteca de classes compartilhada | Sim — sempre | O contexto do chamador é desconhecido; previne deadlocks |
| Controlador/serviço do ASP.NET Core | Não (opcional) | Sem SynchronizationContext presente |
| Aplicação console (.NET 5+) | Não (opcional) | Sem SynchronizationContext presente |
| WPF / WinForms — camada de UI | Depende | Use false para trabalho de I/O; omita ao atualizar estado de UI |
| WPF / WinForms — I/O no ViewModel | Sim | I/O não deve reter o contexto de UI |
| ASP.NET clássico (.NET Framework) | Sim — sempre | Risco de deadlock com AspNetSynchronizationContext |
| Blazor WebAssembly | Sim | Single-threaded; evita sobrecarga de contexto |
| Testes unitários (qualquer executor) | Não (opcional) | Varia; geralmente seguro sem ele no .NET 5+ |
Handlers de eventos async void | Não | Devem retomar na thread de UI para atualizar controles |
Resumo
ConfigureAwait(false) faz uma única coisa: diz ao awaiter para não capturar e restaurar o SynchronizationContext atual. As consequências são:
- Continuações executam no thread pool em vez de serem publicadas de volta ao contexto original.
- Deadlocks são prevenidos em frameworks que têm um contexto e onde alguém está bloqueando na tarefa com
.Resultou.Wait(). - Impacto na performance é insignificante — não use como otimização.
- O tratamento de erros não é afetado — exceções se propagam exatamente da mesma forma.
A regra prática é simples: código de biblioteca usa ConfigureAwait(false) em cada await; código de aplicação ASP.NET Core e console não precisa. Use os analisadores Roslyn CA2007 ou VSTHRD111 para aplicar a regra automaticamente em toda uma base de código.