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:
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:
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:
BackgroundJob.Schedule<IReminderService>(
service => service.SendReminderAsync(orderId),
TimeSpan.FromHours(24));
Recurring Jobs
Run a job on a cron schedule:
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:
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:
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:
[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:
[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:
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:
builder.Services.AddHangfireServer(options =>
{
options.Queues = ["critical", "default", "low"];
});
Assign jobs to specific queues:
_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:
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.