Hangfire is an open-source background job framework for .NET that lets you create, schedule, and process jobs outside the request pipeline with persistent storage, automatic retries, and a built-in monitoring dashboard. It supports four core job types: fire-and-forget jobs that run immediately in the background, delayed jobs that execute after a set time, recurring jobs driven by CRON expressions, and continuations that chain jobs together. Because job definitions are serialized to durable storage such as SQL Server or Redis, jobs survive application restarts and are retried automatically on failure - the default policy retries a failed job up to 10 times with increasing delays. This makes Hangfire the standard choice for offloading work like sending emails, processing files, and generating reports in ASP.NET Core. This guide covers setup, every job type, dependency injection, job filters, dashboard security, production configuration, and monitoring.
Setup
dotnet add package Hangfire.Core
dotnet add package Hangfire.SqlServer
dotnet add package Hangfire.AspNetCore// Program.cs
builder.Services.AddHangfire(config => config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection")));
builder.Services.AddHangfireServer(options =>
{
options.WorkerCount = Environment.ProcessorCount * 2;
options.Queues = new[] { "critical", "default", "low" };
});
var app = builder.Build();
// Dashboard (secure in production!)
app.MapHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() }
});What Job Types Does Hangfire Support?
Fire-and-Forget Jobs
// Execute immediately in background
BackgroundJob.Enqueue(() => Console.WriteLine("Hello, Hangfire!"));
// With service injection
BackgroundJob.Enqueue<IEmailService>(x => x.SendWelcomeEmailAsync("user@example.com"));
// With specific queue
BackgroundJob.Enqueue(() => ProcessOrder(orderId), "critical");Delayed Jobs
// Execute after delay
BackgroundJob.Schedule(() => SendReminderEmail(userId), TimeSpan.FromDays(7));
// Execute at specific time
BackgroundJob.Schedule(() => GenerateMonthlyReport(), new DateTime(2025, 2, 1, 0, 0, 0));
// With service
BackgroundJob.Schedule<INotificationService>(
x => x.SendFollowUpAsync(customerId),
TimeSpan.FromHours(24));Recurring Jobs
// CRON expressions
RecurringJob.AddOrUpdate("daily-cleanup", () => CleanupOldData(), Cron.Daily);
RecurringJob.AddOrUpdate("hourly-sync", () => SyncWithExternalApi(), Cron.Hourly);
RecurringJob.AddOrUpdate("weekly-report", () => GenerateWeeklyReport(), Cron.Weekly);
// Custom CRON: Every Monday at 9 AM
RecurringJob.AddOrUpdate("monday-meeting", () => SendMeetingReminder(), "0 9 * * MON");
// Every 5 minutes
RecurringJob.AddOrUpdate("health-check", () => CheckSystemHealth(), "*/5 * * * *");
// With timezone
RecurringJob.AddOrUpdate(
"daily-backup",
() => BackupDatabase(),
Cron.Daily(hour: 2),
new RecurringJobOptions { TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul") });
// Remove recurring job
RecurringJob.RemoveIfExists("old-job");
// Trigger now (in addition to schedule)
RecurringJob.TriggerJob("daily-cleanup");Continuations
// Chain jobs together
var jobId = BackgroundJob.Enqueue(() => DownloadFile(url));
BackgroundJob.ContinueJobWith(jobId, () => ProcessFile(filePath));
BackgroundJob.ContinueJobWith(jobId, () => NotifyCompletion(userId));
// Multiple continuations
var downloadId = BackgroundJob.Enqueue(() => DownloadLargeFile(url));
var processId = BackgroundJob.ContinueJobWith(downloadId, () => ProcessDownloadedFile());
var notifyId = BackgroundJob.ContinueJobWith(processId, () => SendCompletionEmail());How Do You Use Dependency Injection in Jobs?
// Job service with proper DI
public interface IEmailJobService
{
Task SendWelcomeEmailAsync(string email);
Task SendOrderConfirmationAsync(Guid orderId);
Task SendBulkNewsletterAsync(int campaignId);
}
public class EmailJobService : IEmailJobService
{
private readonly IEmailSender _emailSender;
private readonly IUserRepository _userRepository;
private readonly ILogger<EmailJobService> _logger;
public EmailJobService(
IEmailSender emailSender,
IUserRepository userRepository,
ILogger<EmailJobService> logger)
{
_emailSender = emailSender;
_userRepository = userRepository;
_logger = logger;
}
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
public async Task SendWelcomeEmailAsync(string email)
{
_logger.LogInformation("Sending welcome email to {Email}", email);
await _emailSender.SendAsync(new EmailMessage
{
To = email,
Subject = "Welcome!",
Body = "Thanks for signing up!"
});
}
[Queue("critical")]
public async Task SendOrderConfirmationAsync(Guid orderId)
{
// Process order confirmation
}
[DisableConcurrentExecution(timeoutInSeconds: 300)]
public async Task SendBulkNewsletterAsync(int campaignId)
{
// Only one instance can run at a time
}
}
// Register and use
builder.Services.AddScoped<IEmailJobService, EmailJobService>();
// Enqueue
BackgroundJob.Enqueue<IEmailJobService>(x => x.SendWelcomeEmailAsync("user@example.com"));Job Filters and Attributes
// Retry configuration
[AutomaticRetry(Attempts = 5, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ProcessPaymentAsync(Guid paymentId) { }
// Disable retries
[AutomaticRetry(Attempts = 0)]
public async Task SendOneTimeNotificationAsync(string message) { }
// Queue assignment
[Queue("critical")]
public async Task ProcessUrgentOrderAsync(Guid orderId) { }
// Prevent concurrent execution
[DisableConcurrentExecution(timeoutInSeconds: 60)]
public async Task ImportDataAsync() { }
// Job timeout
[JobExpirationTimeout(hours: 24)]
public async Task LongRunningJobAsync() { }
// Custom filter
public class LoggingJobFilter : JobFilterAttribute, IServerFilter
{
public void OnPerforming(PerformingContext context)
{
Console.WriteLine($"Starting job: {context.BackgroundJob.Id}");
}
public void OnPerformed(PerformedContext context)
{
Console.WriteLine($"Completed job: {context.BackgroundJob.Id}");
}
}
[LoggingJobFilter]
public async Task TrackedJobAsync() { }Batch Jobs (Pro)
// Hangfire.Pro feature - process multiple jobs as a batch
var batchId = BatchJob.StartNew(batch =>
{
foreach (var userId in userIds)
{
batch.Enqueue<IEmailJobService>(x => x.SendNewsletterAsync(userId));
}
});
// Continue after all batch jobs complete
BatchJob.ContinueWith(batchId, batch =>
{
batch.Enqueue(() => SendBatchCompletionReport());
});How Do You Secure the Hangfire Dashboard?
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// Allow in development
if (httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
{
return true;
}
// Check authentication
if (!httpContext.User.Identity?.IsAuthenticated ?? true)
{
return false;
}
// Check role
return httpContext.User.IsInRole("Admin");
}
}
// Configure dashboard
app.MapHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() },
DashboardTitle = "My App - Jobs",
DisplayStorageConnectionString = false
});How Should You Configure Hangfire for Production?
builder.Services.AddHangfire(config => config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(connectionString, new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.FromSeconds(15),
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true,
PrepareSchemaIfNecessary = true,
SchemaName = "hangfire"
}));
builder.Services.AddHangfireServer(options =>
{
options.ServerName = $"{Environment.MachineName}:{Guid.NewGuid():N}";
options.WorkerCount = 20;
options.Queues = new[] { "critical", "default", "low" };
options.StopTimeout = TimeSpan.FromSeconds(30);
options.ShutdownTimeout = TimeSpan.FromSeconds(15);
});Real-World Example: Order Processing
public class OrderProcessingService : IOrderProcessingService
{
private readonly IOrderRepository _orderRepository;
private readonly IPaymentService _paymentService;
private readonly IInventoryService _inventoryService;
private readonly IEmailService _emailService;
private readonly ILogger<OrderProcessingService> _logger;
[Queue("critical")]
[AutomaticRetry(Attempts = 3)]
public async Task ProcessOrderAsync(Guid orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
var order = await _orderRepository.GetByIdAsync(orderId);
if (order == null)
{
_logger.LogWarning("Order {OrderId} not found", orderId);
return;
}
try
{
// Reserve inventory
await _inventoryService.ReserveItemsAsync(order.Items);
// Process payment
var paymentResult = await _paymentService.ChargeAsync(order);
if (!paymentResult.Success)
{
await _inventoryService.ReleaseItemsAsync(order.Items);
throw new PaymentFailedException(paymentResult.Error);
}
// Update order status
order.Status = OrderStatus.Paid;
await _orderRepository.UpdateAsync(order);
// Schedule follow-up jobs
BackgroundJob.Enqueue<IEmailService>(x => x.SendOrderConfirmationAsync(orderId));
BackgroundJob.Schedule<IEmailService>(
x => x.SendReviewRequestAsync(orderId),
TimeSpan.FromDays(7));
_logger.LogInformation("Order {OrderId} processed successfully", orderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw; // Will trigger retry
}
}
}
// Usage in controller
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
var order = await _orderService.CreateAsync(request);
// Process in background
BackgroundJob.Enqueue<IOrderProcessingService>(x => x.ProcessOrderAsync(order.Id));
return Accepted(new { OrderId = order.Id });
}How Do You Monitor Hangfire Jobs?
// Health check for Hangfire
builder.Services.AddHealthChecks()
.AddHangfire(options =>
{
options.MinimumAvailableServers = 1;
options.MaximumJobsFailed = 10;
});
// Custom monitoring
public class HangfireMonitoringService
{
private readonly IMonitoringApi _monitoringApi;
public HangfireMonitoringService()
{
_monitoringApi = JobStorage.Current.GetMonitoringApi();
}
public HangfireStats GetStats()
{
var stats = _monitoringApi.GetStatistics();
return new HangfireStats
{
Enqueued = stats.Enqueued,
Processing = stats.Processing,
Succeeded = stats.Succeeded,
Failed = stats.Failed,
Scheduled = stats.Scheduled,
Recurring = stats.Recurring,
Servers = stats.Servers
};
}
public IEnumerable<FailedJobDto> GetFailedJobs(int count = 10)
{
return _monitoringApi.FailedJobs(0, count)
.Select(j => new FailedJobDto
{
Id = j.Key,
JobName = j.Value.Job?.ToString(),
FailedAt = j.Value.FailedAt,
ExceptionMessage = j.Value.ExceptionMessage
});
}
}Background jobs are crucial for responsive applications. Use fire-and-forget for immediate background work, scheduled jobs for delayed tasks, and recurring jobs for periodic maintenance. Always implement proper retry policies and monitor job health.
Hangfire provides a robust, reliable solution for background job processing. Its persistence guarantees jobs survive application restarts, and the dashboard gives you full visibility into your job processing pipeline.
