Implementando Background Services com C# e .NET
Background services são uma parte fundamental do ecossistema .NET, especialmente em aplicações que precisam executar tarefas em segundo plano, sem depender da interação direta com usuários. Neste artigo, você vai entender o conceito, conhecer as aplicações práticas e implementar um serviço completo usando exemplos em C#.
Quer se aprofundar ainda mais e criar implementações reais com a biblioteca padrão e com bibliotecas de terceiros? Então baixe o eBook gratuito sobre Background Services do balta.io.
O que são background services
Background services são serviços ou tarefas que rodam de forma contínua ou periódica em segundo plano em aplicações .NET. Eles convivem com a API, o site ou o worker principal, mas não dependem de uma requisição do usuário para começar.
Esses serviços surgiram como resposta à necessidade de sistemas robustos, capazes de lidar com operações pesadas sem comprometer a experiência de quem usa a aplicação. Por isso, eles se tornaram peça comum em praticamente qualquer projeto de backend.
Por exemplo, imagine uma loja virtual que precisa enviar e-mails de confirmação, gerar relatórios de vendas diários e processar pagamentos automaticamente. Todas essas operações são essenciais.
No entanto, elas consomem tempo e recursos. Assim, executá-las no mesmo fluxo da requisição do usuário deixaria cada clique lento e frágil.
Os background services resolvem exatamente esse problema. Eles gerenciam essas tarefas de forma eficiente, sem sobrecarregar a interface principal nem prejudicar a performance.
Quando usar um serviço em segundo plano
Na prática, esse recurso se encaixa bem em três situações. Primeiro, no processamento de tarefas longas ou recorrentes, como a consolidação de relatórios.
Além disso, ele atende tarefas que não exigem interação direta com o usuário, como limpeza de cache ou sincronização com sistemas externos. Por fim, ele brilha no processamento de filas e de tarefas agendadas.
Como os background services funcionam no .NET
No .NET, todo serviço em segundo plano é um hosted service. Em outras palavras, trata-se de uma classe que o host da aplicação inicia junto com ela e encerra quando ela para.
Segundo a documentação oficial sobre hosted services, o host ativa cada serviço uma vez, na inicialização, e o desliga de forma graciosa no encerramento. Esse ciclo vale tanto para APIs ASP.NET Core quanto para projetos criados com o template Worker Service.
A interface IHostedService
A base de tudo é a interface IHostedService. Ela define apenas dois métodos: StartAsync, chamado na inicialização, e StopAsync, chamado no encerramento.
Implementar a interface diretamente dá controle total sobre o ciclo de vida. Contudo, também obriga você a gerenciar tokens de cancelamento e tarefas em execução por conta própria.
A classe base BackgroundService
Para simplificar esse trabalho, o .NET oferece a classe abstrata BackgroundService. Ela implementa IHostedService e expõe um único método para sobrescrever, o ExecuteAsync.
Esse método devolve uma Task que representa toda a vida útil do serviço. Quando a aplicação começa a encerrar, o host aciona o token de cancelamento e espera o ExecuteAsync terminar.
No .NET 10, houve uma mudança relevante nesse comportamento. A partir dessa versão, todo o ExecuteAsync roda em uma thread de segundo plano, inclusive o trecho síncrono antes do primeiro await.
Nas versões anteriores, esse trecho inicial rodava na thread principal e bloqueava a inicialização dos outros serviços. Portanto, código pesado no começo do método atrasava a subida da aplicação inteira.
O template Worker Service
Nem todo projeto precisa de uma API para hospedar serviços em segundo plano. Para esses casos, o .NET oferece o template Worker Service, criado com o comando dotnet new worker.
Esse template gera uma aplicação de console com host genérico e uma classe Worker que já herda de BackgroundService. Além disso, o arquivo de projeto usa o SDK Microsoft.NET.Sdk.Worker.
Na prática, o worker recebe os mesmos recursos de uma API: configuração, logging e injeção de dependência. Por isso, ele serve como ponto de partida ideal para processadores de fila, integrações e rotinas noturnas.
Outra vantagem aparece na hora de publicar. Um worker pode rodar como serviço do Windows, como daemon do systemd no Linux ou dentro de um contêiner.
Tipos de background services
Existem diversos tipos de background services no .NET, e cada um atende um cenário. Conhecer as opções ajuda a escolher a abordagem certa antes de escrever a primeira linha.
O tipo mais comum deriva diretamente de BackgroundService e roda de forma contínua. Por isso, ele é ideal para serviços que ficam vivos durante toda a execução da aplicação.
O serviço temporizado, por sua vez, executa periodicamente com um timer. Assim, ele se adapta bem a tarefas recorrentes, como uma verificação a cada cinco minutos.
Há também o serviço baseado em fila com Channel, usado para processar tarefas enfileiradas. Ele combina com aplicações que recebem trabalho em uma requisição e o processam depois.
Os serviços com escopo usam escopos de injeção de dependência. Dessa forma, cada execução recebe instâncias isoladas, o que funciona bem para operações com banco de dados.
Quando o agendamento fica complexo, entram bibliotecas como Hangfire e Quartz. Elas agendam e executam tarefas recorrentes com recursos como persistência, novas tentativas e expressões cron.
Por fim, a implementação direta de IHostedService permite personalizar completamente a execução e o ciclo de vida. Essa opção faz sentido quando a classe base não atende a uma necessidade específica.
Background services temporizados com PeriodicTimer
A documentação mostra o serviço temporizado com System.Threading.Timer. No entanto, ela alerta que esse timer não espera a execução anterior terminar, o que pode causar sobreposição.
Para background services temporizados, uma alternativa moderna é o PeriodicTimer, que aguarda cada ciclo de forma assíncrona. Com isso, uma execução nunca começa antes da anterior acabar.
public class ReportService(ILogger<ReportService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Dispara a cada cinco minutos, sem sobrepor execuções
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
logger.LogInformation("Generating report at {time}", DateTimeOffset.Now);
}
}
}
Se o assunto for agendamento mais elaborado, vale ler os artigos sobre agendamento de tarefas com Quartz e sobre processamento em segundo plano com Hangfire.
Background services com escopo de injeção de dependência
Todo hosted service vive como singleton, e isso vale para todos os background services. Por isso, ele não pode receber diretamente um serviço scoped, como um DbContext, pelo construtor.
A solução consiste em injetar o IServiceScopeFactory e criar um escopo a cada ciclo. Assim, cada execução recebe as próprias instâncias e as descarta ao final.
public class CleanupService(IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Um escopo novo por ciclo, descartado no fim do bloco
await using var scope = scopeFactory.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
await repository.RemoveExpiredAsync(stoppingToken);
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
Se os ciclos de vida ainda geram dúvida, o artigo sobre AddTransient, AddScoped e AddSingleton explica cada um em detalhes.
Background services baseados em fila com Channel
Em background services baseados em fila, a requisição apenas registra o trabalho e responde rápido. Enquanto isso, um serviço em segundo plano consome os itens no próprio ritmo.
public class EmailQueueService(Channel<string> queue, ILogger<EmailQueueService> logger)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Lê os itens assim que chegam, até o cancelamento
await foreach (var email in queue.Reader.ReadAllAsync(stoppingToken))
{
logger.LogInformation("Sending email to {email}", email);
}
}
}
O canal precisa existir como singleton, criado com Channel.CreateUnbounded<string>() ou com uma capacidade limitada. O artigo sobre Channels em C# aprofunda esse padrão em APIs de alto volume.
Implementando background services passo a passo
Agora, veja como criar um BackgroundService simples em C#, explicado bloco por bloco. O exemplo registra uma mensagem a cada minuto até a aplicação encerrar.
Definição da classe e dependências
O primeiro bloco do nosso background service define a classe e recebe as dependências pelo construtor.
public class MyBackgroundService : BackgroundService
{
private readonly ILogger<MyBackgroundService> _logger;
public MyBackgroundService(ILogger<MyBackgroundService> logger)
{
_logger = logger;
}
Aqui, a classe MyBackgroundService herda da classe base BackgroundService. Em seguida, a injeção de dependência entrega uma instância de ILogger, que permite registrar logs durante a execução do serviço.
Método ExecuteAsync
O segundo bloco implementa o método ExecuteAsync, o principal ponto de execução do serviço.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Service started.");
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Service running at: {time}", DateTimeOffset.Now);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
_logger.LogInformation("Service stopped.");
}
}
O host invoca esse método automaticamente quando o background service inicia. Então, ele roda continuamente até que alguém solicite o encerramento, sinalizado pelo stoppingToken.
Durante a execução, o método registra um log periódico indicando que o serviço está ativo. Depois, ele aguarda um minuto antes de repetir o ciclo e, ao ser cancelado, registra que o serviço parou.
O papel do stoppingToken
A propriedade stoppingToken.IsCancellationRequested verifica se o token de cancelamento foi acionado. Quando isso acontece, o serviço sabe que precisa parar.
Assim que o valor se torna verdadeiro, o laço while termina. Dessa forma, o serviço consegue realizar qualquer limpeza necessária antes de finalizar.
Essa abordagem garante que os background services tenham uma interrupção graciosa e controlada. Ela importa principalmente quando o sistema operacional, um orquestrador de contêineres ou o próprio ambiente pede o encerramento.
Repare também que o token vai para o Task.Delay. Com isso, a espera termina imediatamente no cancelamento, em vez de segurar o encerramento por até um minuto.
Nesse caso, o Task.Delay lança uma OperationCanceledException, que o BackgroundService trata como encerramento normal. Por isso, a mensagem final só aparece quando o cancelamento chega durante o log, e não durante a espera.
Registrando o serviço no container
Para que o host conheça o background service, registre-o no container de injeção de dependência com AddHostedService. No modelo atual, isso acontece no arquivo Program.cs.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<MyBackgroundService>();
var app = builder.Build();
app.Run();
Em projetos mais antigos, o mesmo registro fica no método ConfigureServices da classe Startup. O comportamento, porém, continua igual.
Monitoramento e gerenciamento de background services
Um serviço que roda sem ninguém olhar precisa de visibilidade e de proteção contra falhas. Afinal, quando algo dá errado em segundo plano, nenhum usuário recebe uma tela de erro para avisar.
Logging
Use a injeção de dependência para incluir o ILogger e acompanhar o status do serviço. Registre o início, o fim e cada ciclo relevante, sempre com mensagens estruturadas.
Para centralizar esses registros, o artigo sobre Serilog no ASP.NET mostra como enviar logs para destinos externos. Assim, o time acompanha os serviços em produção sem acessar o servidor.
Controle de erros
Implemente um tratamento robusto de exceções dentro do laço principal. Dessa forma, uma falha em um ciclo não derruba o serviço inteiro.
Esse cuidado ficou ainda mais importante a partir do .NET 6. Desde essa versão, uma exceção não tratada no ExecuteAsync encerra o host por padrão, conforme a opção BackgroundServiceExceptionBehavior.StopHost.
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// Registra a falha e segue para o próximo ciclo
_logger.LogError(ex, "Error while processing the cycle");
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
Se preferir o comportamento antigo, configure HostOptions.BackgroundServiceExceptionBehavior como Ignore. Ainda assim, tratar o erro dentro do serviço costuma ser a escolha mais segura e previsível.
Encerramento gracioso
Por padrão, o host espera até 30 segundos para que os serviços terminem. Para aumentar esse prazo, configure a propriedade ShutdownTimeout em HostOptions.
Vale lembrar que o StopAsync pode não executar quando o processo cai de forma inesperada. Portanto, background services que manipulam dados críticos precisam tolerar interrupções a qualquer momento.
Boas práticas para background services
Alguns cuidados simples evitam boa parte dos problemas com background services em produção. Primeiro, mantenha cada serviço com uma única responsabilidade, em vez de concentrar várias tarefas no mesmo laço.
Além disso, repasse o stoppingToken para todas as chamadas assíncronas que aceitam um CancellationToken. Assim, consultas ao banco, requisições HTTP e esperas terminam junto com o serviço.
Evite também trabalho pesado no construtor. Como o host cria a instância durante a inicialização, qualquer lentidão ali atrasa a subida da aplicação inteira.
Outro ponto importante envolve múltiplas instâncias. Quando a aplicação roda em vários servidores ou contêineres, cada cópia executa os próprios background services.
Nesse caso, uma tarefa agendada pode rodar várias vezes ao mesmo tempo. Por isso, tarefas que precisam rodar uma única vez pedem um mecanismo de trava distribuída ou um agendador como Hangfire e Quartz.
Pense ainda na idempotência das operações. Se o processo cair no meio de um ciclo, a próxima execução deve conseguir repetir o trabalho sem duplicar e-mails, pagamentos ou registros.
Por fim, prefira intervalos configuráveis em vez de valores fixos no código. Com o IOptions, você ajusta a frequência de cada serviço por ambiente, sem precisar de uma nova publicação.
Seguindo essas práticas, background services deixam de ser uma caixa-preta e passam a ser componentes previsíveis. Dessa forma, o time ganha confiança para colocar mais tarefas em segundo plano.
Conclusão
Background services são essenciais para qualquer aplicação robusta, porque permitem executar tarefas em segundo plano de forma eficiente e organizada. Com a classe BackgroundService, um token de cancelamento e um registro no container, você já tem um serviço funcional.
Além disso, conhecer os tipos disponíveis ajuda a escolher entre um laço contínuo, um timer, uma fila ou um agendador externo. Dominar essa implementação no .NET contribui diretamente para aplicações escaláveis e confiáveis.
FAQ
Qual a diferença entre IHostedService e BackgroundService?
O IHostedService é a interface com StartAsync e StopAsync. Já o BackgroundService é uma classe base que implementa essa interface e pede apenas o método ExecuteAsync.
Posso injetar um DbContext em um BackgroundService?
Não diretamente, porque o serviço é singleton e o DbContext é scoped. Use o IServiceScopeFactory para criar um escopo a cada execução.
O que acontece se o ExecuteAsync lançar uma exceção?
Desde o .NET 6, o host encerra a aplicação por padrão. Trate os erros dentro do laço ou altere o BackgroundServiceExceptionBehavior.
Como agendar uma tarefa em um horário específico?
Para horários fixos ou expressões cron, prefira bibliotecas como Hangfire ou Quartz. Um PeriodicTimer atende bem intervalos regulares.
Background services funcionam fora do ASP.NET Core?
Sim. O template Worker Service cria uma aplicação de console com host genérico, ideal para rodar apenas serviços em segundo plano.