Toda aplicação backend acumula tarefas que não devem bloquear a resposta HTTP: enviar e-mail de confirmação, gerar PDF, processar pagamento em segundo plano, sincronizar dados com sistemas externos. Hangfire resolve isso com uma API simples, persistência em banco de dados e retry automático — tudo sem configurar filas externas como RabbitMQ.
Setup do Hangfire com PostgreSQL
dotnet add package Hangfire.AspNetCore dotnet add package Hangfire.PostgreSql // ou Hangfire.SqlServer para SQL Server// No Program.cs: builder.Services.AddHangfire(config => config .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UsePostgreSqlStorage(options => options.UseNpgsqlClientFactory( builder.Configuration.GetConnectionString(“Default”))));
// Worker que processa os jobs (pode rodar no mesmo processo ou separado) builder.Services.AddHangfireServer(options => { options.WorkerCount = 5; // threads paralelas options.Queues = [“critical”, “default”, “low”]; // prioridade de filas options.ServerTimeout = TimeSpan.FromMinutes(5); });
// Dashboard (só em dev ou com autenticação!) app.UseHangfireDashboard(“/hangfire”, new DashboardOptions { Authorization = [new HangfireAuthFilter()], // NUNCA exponha sem auth em produção IsReadOnlyFunc = _ => false });
Os 4 tipos de job do Hangfire
1. Fire-and-Forget: dispara e esquece
// Processa assincronamente após retornar HTTP 200 ao cliente [HttpPost("pedidos")] public async Task<IActionResult> CriarPedido([FromBody] CriarPedidoRequest request) { var pedidoId = await _pedidoService.CriarAsync(request);// Dispara job em background — não bloqueia a resposta BackgroundJob.Enqueue<EmailService>( svc => svc.EnviarConfirmacaoPedidoAsync(pedidoId)); // Fila crítica para processamento prioritário BackgroundJob.Enqueue<PagamentoService>( svc => svc.ProcessarAsync(pedidoId), new EnqueuedState("critical")); // fila com maior prioridade return CreatedAtAction(nameof(GetPedido), new { id = pedidoId }, null);
}
2. Delayed: agendado para o futuro
// Cancela pedido se não pago em 30 minutos public void AgendarCancelamentoPedido(Guid pedidoId) { BackgroundJob.Schedule<PedidoService>( svc => svc.CancelarSeNaoPagoAsync(pedidoId), TimeSpan.FromMinutes(30)); }
// Envia lembrete de carrinho abandonado em 2 horas public void AgendarLembreteCarrinho(Guid usuarioId) { BackgroundJob.Schedule<MarketingService>( svc => svc.EnviarLembreteCarrinhoAsync(usuarioId), TimeSpan.FromHours(2)); }
3. Recurring: jobs recorrentes com Cron
// Registrar jobs recorrentes na inicialização (não no loop de request) public class JobsBootstrap : IHostedService { public Task StartAsync(CancellationToken ct) { // Todo dia às 6h: relatório de vendas do dia anterior RecurringJob.AddOrUpdate<RelatorioService>( recurringJobId: "relatorio-diario-vendas", methodCall: svc => svc.GerarRelatorioVendasAsync(), cronExpression: "0 6 * * *", timeZone: TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time")); // Brasília// A cada hora: sincronizar estoques com ERP externo RecurringJob.AddOrUpdate<EstoqueService>( recurringJobId: "sync-estoque-erp", methodCall: svc => svc.SincronizarAsync(), cronExpression: Cron.Hourly()); // Segunda às 8h: limpeza de logs antigos RecurringJob.AddOrUpdate<ManutencaoService>( recurringJobId: "limpeza-logs", methodCall: svc => svc.LimparLogsAntigosAsync(), cronExpression: "0 8 * * 1"); return Task.CompletedTask; } public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
4. Continuations: jobs encadeados
// Encadeia jobs: geração → envio → notificação public void ProcessarRelatorioMensal(int mes, int ano) { var gerarJobId = BackgroundJob.Enqueue<RelatorioService>( svc => svc.GerarAsync(mes, ano));var enviarJobId = BackgroundJob.ContinueJobWith<EmailService>( gerarJobId, svc => svc.EnviarRelatorioAsync(mes, ano)); BackgroundJob.ContinueJobWith<SlackService>( enviarJobId, svc => svc.NotificarAsync($"Relatório {mes}/{ano} enviado"));
}
Implementando jobs idempotentes e resilientes
Jobs podem ser reprocessados (falha, restart do servidor, retry). Seu job deve ser idempotente — executar N vezes deve ter o mesmo efeito que executar uma vez:
public class EmailService { private readonly AppDbContext _db; private readonly IMailer _mailer;// Job idempotente: verifica se já foi enviado antes de enviar [AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 120, 300])] public async Task EnviarConfirmacaoPedidoAsync(Guid pedidoId) { var pedido = await _db.Pedidos .Include(p => p.Cliente) .FirstOrDefaultAsync(p => p.Id == pedidoId); if (pedido == null) { // Job foi criado mas pedido foi deletado — ignora return; } if (pedido.EmailConfirmacaoEnviadoEm.HasValue) { // Já enviado em execução anterior — idempotente, ignora return; } await _mailer.EnviarAsync( to: pedido.Cliente.Email, subject: $"Pedido #{pedido.Numero} confirmado", body: GerarCorpoEmail(pedido)); // Marca como enviado para garantir idempotência pedido.EmailConfirmacaoEnviadoEm = DateTime.UtcNow; await _db.SaveChangesAsync(); }
}
Filas priorizadas: organize por urgência
// Configure o worker com filas em ordem de prioridade: services.AddHangfireServer(options => { options.WorkerCount = 10; // Workers processam "critical" primeiro, depois "default", depois "low" options.Queues = ["critical", "default", "low"]; });// Use atributo para definir a fila do job: [Queue(“critical”)] public class PagamentoService { public async Task ProcessarAsync(Guid pedidoId) { /* … */ } }
[Queue(“low”)] public class RelatorioService { public async Task GerarAsync(int mes, int ano) { /* … */ } }
// Ou defina a fila na hora do enqueue: BackgroundJob.Enqueue<NotificacaoService>( svc => svc.EnviarPushAsync(usuarioId), new EnqueuedState(“low”));
Monitoramento e alertas de jobs com falha
// Filtro global para notificar quando jobs falham definitivamente public class AlertaFalhaJobFilter : JobFilterAttribute, IElectStateFilter { private readonly ISlackClient _slack;public void OnStateElection(ElectStateContext context) { if (context.CandidateState is FailedState failedState) { // Job esgotou todas as tentativas de retry var jobName = context.BackgroundJob.Job.Type.Name; var mensagem = $"❌ Job falhou definitivamente: {jobName}\n" + $"ID: {context.BackgroundJob.Id}\n" + $"Erro: {failedState.Exception.Message}"; // Notifica Slack, PagerDuty, etc. _slack.PostMessageAsync("#alertas-producao", mensagem).GetAwaiter().GetResult(); } }}
// Registrar o filtro globalmente: GlobalJobFilters.Filters.Add(new AlertaFalhaJobFilter(slackClient));
// Adicionar logging automático com ILogger: public class LoggingJobFilter : JobFilterAttribute, IServerFilter { private readonly ILogger<LoggingJobFilter> _logger;
public void OnPerforming(PerformingContext context) => _logger.LogInformation("Iniciando job {JobId}: {Job}", context.BackgroundJob.Id, context.BackgroundJob.Job); public void OnPerformed(PerformedContext context) => _logger.LogInformation("Job {JobId} concluído em {Duration}ms", context.BackgroundJob.Id, (DateTime.UtcNow - context.BackgroundJob.CreatedAt).TotalMilliseconds);
}
Workers em processo separado (produção escalável)
Para sistemas de alto volume, separe a API dos workers em processos distintos:
// Projeto "MinhaApp.Workers" — Worker Service separado da API // Program.cs do worker: var host = Host.CreateDefaultBuilder(args) .ConfigureServices((ctx, services) => { services.AddHangfire(config => config.UsePostgreSqlStorage(options => options.UseNpgsqlClientFactory( ctx.Configuration.GetConnectionString("Default"))));// Apenas o server (sem dashboard, sem API endpoints) services.AddHangfireServer(options => { options.WorkerCount = Environment.ProcessorCount * 2; options.Queues = ["critical", "default", "low"]; }); // Registrar os serviços usados pelos jobs services.AddScoped<EmailService>(); services.AddScoped<RelatorioService>(); services.AddDbContext<AppDbContext>(...); }) .Build();await host.RunAsync();
// Escale horizontalmente: rode N instâncias do Worker em containers // Hangfire gerencia a distribuição — nenhum job executa duas vezes
Quando usar Hangfire vs Quartz.NET vs channels
Hangfire persiste jobs no banco de dados — sobrevive a restarts. Ideal para: envio de e-mails, processamento de relatórios, jobs agendados com histórico e retry. Quartz.NET é mais robusto para scheduling complexo com regras de calendário. Para processamento de stream em memória (alta taxa, sem necessidade de persistência), use System.Threading.Channels ou MassTransit.
Conclusão
Hangfire simplifica background jobs sem infraestrutura adicional — você já tem um banco de dados, o Hangfire usa ele. Os padrões de idempotência, filas priorizadas e workers separados cobrem 95% dos casos em produção. O dashboard visual é um diferencial para debugar e reprocessar jobs com falha manualmente.
Se você precisa implementar processamento assíncrono em sua aplicação .NET — de e-mails a pipelines de dados — a Neryx pode projetar e implementar a solução correta para o seu volume. Consultoria inicial gratuita.
Leitura complementar: