Hangfire solves a specific problem: running background work reliably. It persists jobs to a database, retries failures automatically, and provides a dashboard to monitor everything. Unlike Task.Run or BackgroundService, Hangfire survives application restarts — if a job is queued, it will run eventually.

Setup

dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer

Configure in Program.cs:

Program.cs
builder.Services.AddHangfire(config =>
{
    config.UseSqlServerStorage(
        builder.Configuration.GetConnectionString("Hangfire"));
});

builder.Services.AddHangfireServer();

// After building the app
app.UseHangfireDashboard("/hangfire");

Hangfire creates its own tables in the specified database. The dashboard at /hangfire shows job status, retries, and failures.

Job Types

Fire-and-Forget

Queue a job to run as soon as a worker is available:

Example.cs
BackgroundJob.Enqueue<IEmailService>(
    service => service.SendWelcomeEmailAsync("[email protected]"));

Hangfire serialises the method call and its arguments. When a worker picks it up, it resolves IEmailService from DI and calls the method.

Delayed Jobs

Schedule a job to run after a delay:

Example.cs
BackgroundJob.Schedule<IReminderService>(
    service => service.SendReminderAsync(orderId),
    TimeSpan.FromHours(24));

Recurring Jobs

Run a job on a cron schedule:

Example.cs
RecurringJob.AddOrUpdate<IReportService>(
    "daily-report",
    service => service.GenerateDailyReportAsync(),
    Cron.Daily(8, 0));  // Every day at 08:00

Continuations

Chain jobs so one runs after another completes:

Example.cs
var jobId = BackgroundJob.Enqueue<IOrderService>(
    service => service.ProcessOrderAsync(orderId));

BackgroundJob.ContinueJobWith<INotificationService>(
    jobId,
    service => service.NotifyOrderProcessedAsync(orderId));

Using DI Properly

The examples above use static methods for simplicity, but in practice you should inject IBackgroundJobClient and IRecurringJobManager:

Example.cs
public class OrderController : ControllerBase
{
    private readonly IBackgroundJobClient _jobClient;

    public OrderController(IBackgroundJobClient jobClient)
    {
        _jobClient = jobClient;
    }

    [HttpPost]
    public IActionResult CreateOrder(CreateOrderRequest request)
    {
        var orderId = Guid.NewGuid();

        _jobClient.Enqueue<IOrderProcessor>(
            processor => processor.ProcessAsync(orderId));

        return Accepted(new { OrderId = orderId });
    }
}

This is testable — you can mock IBackgroundJobClient to verify jobs are enqueued without actually running them.

Automatic Retries

By default, Hangfire retries failed jobs 10 times with increasing delays. Customise per job:

Example.cs
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ProcessPaymentAsync(Guid paymentId)
{
    // If this throws, Hangfire retries up to 3 times
    var payment = await _paymentGateway.ChargeAsync(paymentId);
    await _repository.UpdateAsync(payment);
}

Or disable retries entirely:

Example.cs
[AutomaticRetry(Attempts = 0)]
public async Task SendNotificationAsync(string userId, string message)
{
    await _pushService.SendAsync(userId, message);
}

Job Filters

Hangfire supports filters for cross-cutting concerns, similar to ASP.NET Core filters:

Example.cs
public class LoggingJobFilter : JobFilterAttribute, IElectStateFilter
{
    private readonly ILogger<LoggingJobFilter> _logger;

    public LoggingJobFilter(ILogger<LoggingJobFilter> logger)
    {
        _logger = logger;
    }

    public void OnStateElection(ElectStateContext context)
    {
        if (context.CandidateState is FailedState failedState)
        {
            _logger.LogError(failedState.Exception,
                "Job {JobId} failed", context.BackgroundJob.Id);
        }
    }
}

Queues

Organise jobs into queues with different priorities:

Example.cs
builder.Services.AddHangfireServer(options =>
{
    options.Queues = ["critical", "default", "low"];
});

Assign jobs to specific queues:

Example.cs
_jobClient.Enqueue<IPaymentService>(
    service => service.ProcessRefundAsync(refundId));

// Or with a queue attribute on the method:
[Queue("critical")]
public async Task ProcessRefundAsync(Guid refundId) { }

Workers process queues in order — all critical jobs run before default.

Dashboard Authorisation

In production, lock down the dashboard:

Example.cs
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization =
    [
        new HangfireAuthorizationFilter()
    ]
});

public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();
        return httpContext.User.IsInRole("Admin");
    }
}

Hangfire excels at jobs triggered by user actions — sending emails, processing uploads, generating reports. For time-based scheduling with complex cron rules, Quartz.NET may be a better fit. Many projects use both: Quartz.NET for scheduled work and Hangfire for on-demand background processing.