Background Services em C# e .NET
Background services são processos que rodam em segundo plano em aplicações .NET, fora do ciclo tradicional de requisição e resposta HTTP. Em aplicações modernas, eles cuidam de tudo aquilo que não pode travar o usuário enquanto ele espera uma resposta.
Neste artigo, você vai entender o que são background services, quais são os principais tipos disponíveis na plataforma e quando utilizar cada um. Além disso, você verá exemplos em C# e boas práticas para manter esses serviços estáveis em produção.
O que são background services
Um background service é uma tarefa executada de forma assíncrona no backend da aplicação, geralmente com propósito contínuo ou recorrente. Ele não depende de uma requisição HTTP para começar a trabalhar.
Na prática, o serviço continua rodando enquanto a aplicação estiver viva. O host do .NET inicia o serviço junto com a aplicação e o encerra quando ela recebe o sinal de desligamento.
No ecossistema .NET, essa ideia recebe o nome técnico de hosted service. Segundo a documentação oficial sobre hosted services no ASP.NET Core, um hosted service é uma classe com lógica de tarefa em segundo plano que implementa a interface IHostedService.
Os background services aparecem com frequência em cenários como estes:
- Enviar e-mails automaticamente após um cadastro ou uma compra.
- Processar filas de mensageria sem bloquear a API.
- Executar tarefas agendadas, no estilo dos cron jobs do Linux.
- Sincronizar dados com APIs externas em intervalos regulares.
- Monitorar recursos, eventos ou o estado de outros sistemas.
Por isso, quase toda aplicação de médio porte acaba precisando de pelo menos um serviço desse tipo. Ainda assim, existem formas diferentes de implementar background services, e cada uma resolve um problema específico.
Por que usar background services em vez da requisição
Imagine uma API que, ao receber um pedido, precisa gravar os dados, gerar uma nota fiscal e enviar um e-mail. Se tudo acontecer dentro da mesma requisição, o cliente espera por operações que não precisam terminar antes da resposta.
Com isso, a latência aumenta e qualquer falha no envio do e-mail pode derrubar a requisição inteira. Os background services resolvem esse problema porque tiram o trabalho lento do caminho crítico.
Além disso, eles permitem tratar falhas de forma isolada. Se o servidor de e-mail estiver fora do ar, o serviço tenta de novo mais tarde, enquanto a API continua respondendo normalmente.
Em outras palavras, a requisição fica responsável apenas pelo essencial. Todo o resto vai para o segundo plano.
Os principais tipos de background services no .NET
A plataforma oferece uma base comum, mas o uso no dia a dia gera alguns padrões bem conhecidos. A seguir, você vê cada tipo, um exemplo de código e a indicação de quando usar.
BackgroundService com ExecuteAsync
Esse é o tipo mais direto. Você herda da classe abstrata BackgroundService e implementa o método ExecuteAsync.
public class MyWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Lógica de negócio
await Task.Delay(1000, stoppingToken);
}
}
}
A classe BackgroundService já implementa IHostedService e cuida da parte repetitiva do ciclo de vida. Dessa forma, você escreve apenas a lógica do trabalho em si.
O método ExecuteAsync retorna uma Task que representa toda a vida útil do serviço. Quando a aplicação começa a desligar, o host dispara o stoppingToken e espera o método terminar.
Por esse motivo, o laço precisa respeitar o token de cancelamento. Caso contrário, o serviço é interrompido de forma abrupta quando o tempo limite de desligamento acaba.
Para registrar o serviço, basta uma linha no Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Registra o worker como hosted service
builder.Services.AddHostedService<MyWorker>();
var app = builder.Build();
app.Run();
Quando usar: em tarefas contínuas ou com polling, ou seja, checagens regulares de algum recurso.
Timed background services
Os timed background services executam uma ação em intervalos fixos. O exemplo clássico da documentação usa a classe System.Threading.Timer para disparar um método de trabalho.
Contudo, a própria Microsoft alerta que o Timer não espera a execução anterior terminar. Assim, se uma execução demorar mais do que o intervalo, duas execuções podem rodar ao mesmo tempo.
Uma alternativa moderna é o PeriodicTimer, que funciona com await e evita essa sobreposição:
public class ReportWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Dispara a cada 10 minutos
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
// Verifica se existem novos dados para processar
await CheckNewDataAsync(stoppingToken);
}
}
private Task CheckNewDataAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
Você também pode recorrer a bibliotecas como o Quartz.NET quando precisar de mais robustez. Falaremos delas mais adiante.
Quando usar: em ações baseadas em tempo fixo, por exemplo, verificar novos dados a cada 10 minutos. Esse é um dos usos mais comuns de background services em sistemas de integração.
Background services baseados em fila
Nesse modelo de background services, você usa Channel<T>, filas internas ou mensageria externa para desacoplar a geração de tarefas do seu processamento. Enquanto a API apenas enfileira o trabalho, o serviço em segundo plano consome a fila.
public class EmailQueueService
{
private readonly Channel<EmailMessage> _queue = Channel.CreateUnbounded<EmailMessage>();
public async Task QueueEmailAsync(EmailMessage message) =>
await _queue.Writer.WriteAsync(message);
public ChannelReader<EmailMessage> Reader => _queue.Reader;
}
Em seguida, um worker lê a fila de forma contínua:
public class EmailSenderWorker(EmailQueueService queue) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Aguarda mensagens sem consumir CPU enquanto a fila está vazia
await foreach (var message in queue.Reader.ReadAllAsync(stoppingToken))
{
// Envia o e-mail
}
}
}
Como os dois lados compartilham a mesma fila, o EmailQueueService precisa ser registrado como Singleton. Se quiser entender melhor os canais, vale ler o artigo sobre Channels em C# para APIs de alto volume.
Vale lembrar que uma fila em memória se perde quando a aplicação reinicia. Portanto, para mensagens que não podem sumir, use uma mensageria externa como RabbitMQ ou Azure Service Bus.
Quando usar: para processar tarefas à medida que chegam, como envio de e-mails, logs ou eventos de domínio.
Scoped background services
Por padrão, o host não cria nenhum escopo de injeção de dependência para um hosted service. Isso significa que você não pode injetar diretamente um serviço Scoped, como um DbContext, no construtor do worker.
A solução é criar escopos manuais dentro do BackgroundService:
using (var scope = _serviceProvider.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
await service.DoWorkAsync();
}
A documentação mais recente recomenda o CreateAsyncScope, que descarta corretamente serviços assíncronos:
public class ScopedWorker(IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Um novo escopo por ciclo garante um DbContext novo
await using var scope = scopeFactory.CreateAsyncScope();
var service = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
await service.DoWorkAsync();
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
Assim, cada ciclo recebe instâncias novas, e o contexto do banco não acumula entidades rastreadas. Para relembrar a diferença entre os tempos de vida, consulte o artigo sobre AddTransient, AddScoped e AddSingleton.
Quando usar: quando o seu serviço precisa de uma dependência que não pode ser Singleton. Na prática, a maioria dos background services que acessam banco de dados segue esse padrão.
Background services com agendamento
Frameworks como Quartz.NET, Hangfire e Coravel oferecem recursos robustos para tarefas agendadas e cron jobs. Eles funcionam sobre a mesma infraestrutura de hosted services, mas acrescentam várias camadas de controle.
Com eles, você pode:
- Executar tarefas em horários específicos com expressões cron.
- Persistir jobs em banco de dados.
- Monitorar execuções com dashboards.
- Aplicar retries automáticos quando um job falha.
Por exemplo, o Hangfire grava os jobs em um armazenamento persistente e oferece um painel web para acompanhar as execuções. Já o Quartz.NET se destaca em agendamentos complexos, com calendários e gatilhos combinados.
Aqui no balta.io existem guias práticos sobre o agendamento de tarefas com Quartz e sobre processamentos em segundo plano com Hangfire.
Quando usar: quando você precisa de controle fino, agendamentos complexos ou persistência dos jobs.
Implementação manual de IHostedService
Você pode implementar IHostedService diretamente se quiser controle total sobre o ciclo de vida do serviço. A interface define apenas dois métodos.
public class MyHostedService : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
// Inicialização
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
// Encerramento
return Task.CompletedTask;
}
}
O host chama StartAsync durante a inicialização e StopAsync durante o desligamento. Nesse caso, porém, a responsabilidade de iniciar e cancelar trabalhos longos fica toda com você.
Por isso, evite colocar um laço infinito dentro de StartAsync. Se fizer isso, a aplicação nunca termina de iniciar.
Quando usar: quando a lógica de start e stop é independente de um ExecuteAsync ou quando você precisa de algo mais customizado.
Ciclo de vida dos background services
Entender o ciclo de vida evita boa parte dos problemas com background services. O host inicia os hosted services na ordem em que foram registrados e os encerra na ordem inversa.
No .NET 10, a Microsoft mudou um comportamento importante. Antes, a parte síncrona do ExecuteAsync, aquela que vem antes do primeiro await, rodava na thread principal e bloqueava a inicialização dos outros serviços.
Agora, todo o ExecuteAsync roda em uma thread de segundo plano. Segundo a nota de breaking change do .NET 10, quem precisar executar algo antes do restante pode sobrescrever o StartAsync ou implementar IHostedLifecycleService.
Além disso, existe um tempo limite para o desligamento. Se o serviço não terminar dentro dele, o host simplesmente segue em frente e encerra o processo.
Worker Service como projeto dedicado
Nem todo serviço em segundo plano precisa morar dentro de uma API. O .NET oferece o template Worker Service, que cria um projeto focado apenas em processos de longa duração.
dotnet new worker -o ContosoWorker
Esse projeto usa o SDK Microsoft.NET.Sdk.Worker e já traz um Worker herdando de BackgroundService. Dessa forma, você pode rodar o processo como serviço do Windows, daemon do systemd ou contêiner.
Separar o worker da API também facilita a escala. Afinal, você pode ter várias instâncias da API e apenas uma do processador de filas, ou o contrário.
Boas práticas para background services
Alguns cuidados fazem toda a diferença na estabilidade dos background services em produção. Primeiro, sempre propague o CancellationToken para chamadas assíncronas, como consultas ao banco e requisições HTTP.
Em segundo lugar, trate exceções dentro do laço. Desde o .NET 6, uma exceção não tratada em um BackgroundService encerra o host por padrão, então um try/catch com log é essencial.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// Registra o erro e mantém o serviço vivo
logger.LogError(ex, "Error while processing background work");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
Também vale pensar em múltiplas instâncias. Se a aplicação rodar em três servidores, cada um executa o seu próprio hosted service, e um job agendado pode acontecer três vezes.
Nesse caso, use um mecanismo de trava distribuída ou uma ferramenta com persistência, como Hangfire e Quartz.NET em modo cluster. Por fim, monitore os serviços com logs, métricas e health checks.
Como testar background services
Testar background services parece difícil à primeira vista, porque o código roda em um laço controlado pelo host. No entanto, uma regra simples resolve boa parte do problema: mantenha a lógica de negócio fora do worker.
Em vez de escrever toda a regra dentro do ExecuteAsync, mova o trabalho para uma classe comum, como um IMyScopedService. Assim, você testa essa classe com testes de unidade, sem precisar subir o host.
O worker passa a ser apenas um orquestrador. Ele cria o escopo, chama o serviço e espera o próximo ciclo.
Para testar o próprio worker, você pode chamar StartAsync e StopAsync diretamente em um teste. Dessa forma, você verifica se o serviço respeita o cancelamento e encerra dentro do tempo esperado.
Além disso, evite depender do relógio real. Injetar TimeProvider, disponível desde o .NET 8, permite simular a passagem do tempo nos testes de serviços periódicos.
Por fim, os testes de integração com WebApplicationFactory ajudam a validar o conjunto. Nesse caso, a aplicação sobe com todos os background services registrados, e você observa os efeitos no banco ou na fila.
Como escolher entre os tipos de background services
A escolha depende do gatilho do trabalho e do nível de controle que você precisa. A tabela abaixo resume cada cenário.
| Tipo | Ideal para |
|---|---|
BackgroundService |
Tarefas contínuas e polling |
| Timed service | Intervalos fixos |
| Baseado em fila | Processamento desacoplado e mensageria |
| Scoped service | Uso de dependências Scoped |
| Quartz.NET ou Hangfire | Cron jobs e tarefas agendadas |
IHostedService direto |
Controle manual do ciclo de vida |
Em geral, comece pelo BackgroundService, porque ele cobre a maioria dos casos com pouco código. Depois, evolua para filas ou agendadores quando surgir a necessidade de persistência ou de horários complexos.
Se quiser ir além, o balta.io oferece um eBook gratuito sobre background services, com implementações reais usando tanto a biblioteca padrão quanto bibliotecas de terceiros. Você pode baixar o eBook de Background Services e também conferir o passo a passo em implementando background services com C#.
Conclusão
Os background services são aliados poderosos no ecossistema .NET, porque permitem que sua aplicação seja reativa, resiliente e escalável. Eles tiram do caminho da requisição tudo aquilo que pode esperar.
Entender os diferentes tipos disponíveis ajuda você a escolher a melhor abordagem para cada cenário. Assim, você evita tanto o excesso de complexidade quanto as soluções frágeis que quebram no primeiro pico de carga.
FAQ
Qual a diferença entre IHostedService e BackgroundService?
IHostedService é a interface base, com os métodos StartAsync e StopAsync. Já BackgroundService é uma classe abstrata que implementa essa interface e expõe apenas o ExecuteAsync.
Posso injetar um DbContext em um background service?
Não diretamente, porque o DbContext é Scoped e o hosted service vive como Singleton. Crie um escopo com IServiceScopeFactory a cada ciclo de trabalho.
O que acontece se um BackgroundService lançar uma exceção?
Desde o .NET 6, o comportamento padrão é encerrar o host. Por isso, trate as exceções dentro do laço e registre os erros em log.
Background services funcionam com várias instâncias da aplicação?
Funcionam, mas cada instância executa sua própria cópia do serviço. Para jobs que devem rodar uma única vez, use trava distribuída ou um agendador com persistência.
Preciso de uma biblioteca externa para agendar tarefas?
Não, porque PeriodicTimer atende intervalos simples. Para expressões cron, persistência e dashboards, Quartz.NET e Hangfire são opções mais completas.