Hangfire .NET C# Background Jobs Backend

Background jobs em .NET com Hangfire: scheduling, retries e workers em produção

Guia prático de background jobs no .NET com Hangfire: fire-and-forget, jobs recorrentes com Cron, retries automáticos.

N
Neryx Digital Architects
20 de setembro de 2025
11 min de leitura
220 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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&lt;EmailService&gt;(
    svc => svc.EnviarConfirmacaoPedidoAsync(pedidoId));

// Fila crítica para processamento prioritário
BackgroundJob.Enqueue&lt;PagamentoService&gt;(
    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&lt;EstoqueService&gt;(
        recurringJobId: "sync-estoque-erp",
        methodCall: svc => svc.SincronizarAsync(),
        cronExpression: Cron.Hourly());

    // Segunda às 8h: limpeza de logs antigos
    RecurringJob.AddOrUpdate&lt;ManutencaoService&gt;(
        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&lt;EmailService&gt;(
    gerarJobId,
    svc => svc.EnviarRelatorioAsync(mes, ano));

BackgroundJob.ContinueJobWith&lt;SlackService&gt;(
    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&lt;EmailService&gt;();
    services.AddScoped&lt;RelatorioService&gt;();
    services.AddDbContext&lt;AppDbContext&gt;(...);
})
.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:

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.