Hangfire Background Jobs: Reliable Task Processing in .NET
Backend13 min read

Hangfire Background Jobs: Reliable Task Processing in .NET

Master background job processing with Hangfire. Fire-and-forget, scheduled, recurring jobs, and production deployment strategies.

Taha Kocal

Taha Kocal

Full Stack Developer

Apr 18, 2025
#Hangfire#.NET#Background Jobs#Scheduling#C#

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

bash
dotnet add package Hangfire.Core
dotnet add package Hangfire.SqlServer
dotnet add package Hangfire.AspNetCore
csharp
// 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

csharp
// 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

csharp
// 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

csharp
// 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

csharp
// 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?

csharp
// 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

csharp
// 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)

csharp
// 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?

csharp
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?

csharp
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

csharp
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?

csharp
// 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.

Share this article

Related Articles