29. Introduction to Hangfire
About this chapter
Move slow, non-critical, and long-running tasks out of HTTP requests using Hangfire, improving API responsiveness while ensuring work gets done reliably.
- Background processing concepts: When and why to use background jobs
- Slow tasks: Email, reports, image processing, batch imports
- Non-critical tasks: Notifications, audit logs, external syncs
- Hangfire fundamentals: Job enqueueing, workers, and retry logic
- Installation and setup: Adding Hangfire packages and configuration
- Job types: Fire-and-forget, delayed, recurring jobs
Learning outcomes:
- Identify tasks suitable for background processing
- Install and configure Hangfire in ASP.NET Core
- Enqueue background jobs from controllers
- Understand Hangfire’s storage and worker architecture
- Monitor job status and execution
- Implement retry logic and error handling
29.1 Understanding Background Processing Needs
Some tasks don’t belong in HTTP request/response cycles:
Slow Tasks:
- Sending emails (can take seconds)
- Generating large reports (minutes)
- Processing images/videos (minutes to hours)
- Batch data imports (minutes to hours)
Non-Critical Tasks:
- Sending notifications
- Logging audit trails asynchronously
- Syncing data to external systems
- Cleanup jobs (old data, temp files)
Long-Running Tasks:
- Data migrations
- Batch processing
- Scheduled maintenance
If you do these in your HTTP handler, your API response is slow. Users wait. That’s bad.
Solution: Hangfire
Hangfire lets you offload work to background jobs. User requests return immediately. Jobs run asynchronously, retrying on failure.
User request → API enqueues job → Returns 202 Accepted
↓
Background worker picks up job
↓
Executes task (send email, generate report, etc.)
↓
Stores result/status
29.2 Installing Hangfire
Add the necessary packages:
dotnet add package Hangfire.Core
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.PostgreSql
These provide:
- Hangfire.Core: Job queue, execution, retry logic
- Hangfire.AspNetCore: ASP.NET Core integration
- Hangfire.PostgreSql: Persistent storage in PostgreSQL
29.3 Configuring Hangfire
Update Program.cs:
using Hangfire;
using Hangfire.PostgreSql;
var builder = WebApplication.CreateBuilder(args);
// Add Hangfire
builder.Services.AddHangfire(config =>
{
config.UsePostgreSqlStorage(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("PostgreSqlConnection");
});
});
// Add Hangfire server
builder.Services.AddHangfireServer();
builder.Services.AddControllers();
builder.Services.AddScoped<ICommandRepository, CommandRepository>();
builder.Services.AddScoped<IEmailService, EmailService>();
var app = builder.Build();
// Map Hangfire dashboard
app.MapHangfireDashboard("/hangfire");
app.MapControllers();
app.Run();
Hangfire creates tables in your database for storing jobs, their status, and execution history. On first run, you might see:
INFO: Creating Hangfire schema objects in database 'commandapi'...
That’s normal and expected.
29.4 The Hangfire Dashboard
Access the dashboard:
https://localhost:7213/hangfire
You’ll see:
- Jobs list: All jobs (pending, processing, completed, failed)
- Recurring jobs: Scheduled jobs that repeat
- Servers: Workers processing jobs
- Job details: Execution time, retries, exceptions
- Statistics: Job counts, processing speed
It’s invaluable for debugging and monitoring.
29.5 Job Types
Hangfire supports three job patterns:
Fire-and-Forget
Enqueue a job to run ASAP. You don’t care about the result.
// Enqueue a job
BackgroundJob.Enqueue<IEmailService>(service =>
service.SendEmailAsync("user@example.com", "Welcome!", "Thanks for signing up"));
// Response returns immediately
return Accepted();
Job runs in the background. User doesn’t wait.
Delayed Jobs
Schedule a job to run at a specific time.
// Schedule job for 30 minutes from now
BackgroundJob.Schedule<IEmailService>(
service => service.SendReminderEmailAsync("user@example.com"),
TimeSpan.FromMinutes(30));
// Response returns immediately
return Accepted("Reminder scheduled");
Useful for:
- “Remind me in X minutes”
- Batch processing at off-peak hours
- Scheduling cleanup jobs
Recurring Jobs
Schedule a job to run repeatedly (like a cron job).
// Configure in Program.cs
var backgroundJobClient = app.Services.GetRequiredService<IBackgroundJobClient>();
var recurringJobManager = app.Services.GetRequiredService<IRecurringJobManager>();
// Run every day at 2 AM
recurringJobManager.AddOrUpdate<IDataCleanupService>(
"daily-cleanup",
service => service.CleanupOldLogsAsync(),
Cron.Daily(2));
// Run every 5 minutes
recurringJobManager.AddOrUpdate<IHealthCheckService>(
"health-check",
service => service.CheckServiceHealthAsync(),
Cron.Minutely(5));
// Run every Monday at 9 AM
recurringJobManager.AddOrUpdate<IReportService>(
"weekly-report",
service => service.GenerateWeeklyReportAsync(),
Cron.WeeklyMonday(9));
Common cron patterns:
Cron.Minutely() // Every minute
Cron.Hourly() // Every hour
Cron.Daily(hour, minute) // Daily at specific time
Cron.Weekly() // Every week
Cron.Monthly() // Every month
Cron.Yearly() // Every year
Cron.Never() // Disabled
29.6 Simple Fire-and-Forget Example
// Services/IEmailService.cs
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body);
Task SendBulkEmailAsync(List<string> recipients, string subject, string body);
}
// Services/EmailService.cs
public class EmailService : IEmailService
{
private readonly ILogger<EmailService> _logger;
private readonly IConfiguration _config;
public EmailService(ILogger<EmailService> logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
public async Task SendEmailAsync(string to, string subject, string body)
{
try
{
_logger.LogInformation("Sending email to {To} with subject {Subject}", to, subject);
// Your SMTP implementation here
using var smtpClient = new System.Net.Mail.SmtpClient(
_config["Email:SmtpHost"],
int.Parse(_config["Email:SmtpPort"]!));
smtpClient.Credentials = new System.Net.NetworkCredential(
_config["Email:Username"],
_config["Email:Password"]);
var mailMessage = new System.Net.Mail.MailMessage
{
From = new System.Net.Mail.MailAddress(_config["Email:FromAddress"]!),
Subject = subject,
Body = body,
IsBodyHtml = true
};
mailMessage.To.Add(to);
await Task.Run(() => smtpClient.Send(mailMessage));
_logger.LogInformation("Email sent successfully to {To}", to);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email to {To}", to);
throw; // Hangfire will retry
}
}
public async Task SendBulkEmailAsync(List<string> recipients, string subject, string body)
{
_logger.LogInformation("Sending bulk email to {RecipientCount} recipients", recipients.Count);
foreach (var recipient in recipients)
{
await SendEmailAsync(recipient, subject, body);
}
}
}
// Controllers/CommandsController.cs
[ApiController]
[Route("api/[controller]")]
public class CommandsController : ControllerBase
{
private readonly ICommandRepository _repository;
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly ILogger<CommandsController> _logger;
public CommandsController(
ICommandRepository repository,
IBackgroundJobClient backgroundJobClient,
ILogger<CommandsController> logger)
{
_repository = repository;
_backgroundJobClient = backgroundJobClient;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> CreateCommand(CommandMutateDto dto)
{
var command = new Command
{
HowTo = dto.HowTo,
CommandLine = dto.CommandLine,
PlatformId = dto.PlatformId
};
await _repository.CreateCommandAsync(command);
await _repository.SaveChangesAsync();
// Enqueue email notification
var jobId = _backgroundJobClient.Enqueue<IEmailService>(service =>
service.SendEmailAsync(
"admin@example.com",
"New Command Created",
$"Command '{command.HowTo}' was created"));
_logger.LogInformation("Email notification enqueued with job ID {JobId}", jobId);
return CreatedAtAction(nameof(GetCommandById), new { id = command.Id }, command);
}
private IActionResult GetCommandById(int id)
{
return Ok(); // Placeholder
}
}
When /api/commands POST is called:
- Command is created immediately
- Email notification is enqueued (not sent yet)
- API returns 201 Created
- Background worker picks up the job and sends the email asynchronously
29.7 Handling Job Failures & Retries
Hangfire retries failed jobs automatically:
// By default: retries up to 10 times with exponential backoff
// You can customize this:
var jobId = _backgroundJobClient.Enqueue<IEmailService>(
service => service.SendEmailAsync("user@example.com", "Subject", "Body"));
// Or specify custom retry count
_backgroundJobClient.Create<IEmailService>(
service => service.SendEmailAsync("user@example.com", "Subject", "Body"),
new EnqueuedState { });
Retry logic:
- First failure: Retry immediately
- Second failure: Retry after 10 seconds
- Third failure: Retry after 100 seconds
- Continue exponentially until max retries reached
After all retries fail, the job moves to the Failed state in the dashboard.
29.8 Configuring Persistent Storage
Your current configuration stores jobs in PostgreSQL. To ensure jobs survive service restarts:
// appsettings.json
{
"ConnectionStrings": {
"PostgreSqlConnection": "Server=localhost;Port=5432;Database=commandapi;User Id=postgres;Password=password;",
"HangfireConnection": "Server=localhost;Port=5432;Database=hangfire;User Id=postgres;Password=password;"
}
}
// Program.cs - Optional: Use separate database for Hangfire
builder.Services.AddHangfire(config =>
{
config.UsePostgreSqlStorage(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("HangfireConnection");
});
// Optional: Set job expiration (old jobs are auto-deleted)
config.UseFilter(new AutomaticRetryAttribute { Attempts = 5 });
});
Key configurations:
- Persistent Storage: Jobs survive application restarts
- Automatic Cleanup: Old completed jobs deleted after 1 day
- Distributed Workers: Multiple servers can process the same queue
- Priority Queues: Process some jobs before others
29.9 Monitoring & Debugging
View job details in dashboard:
- Navigate to
https://localhost:7213/hangfire - Click on a job to see:
- Input parameters
- Execution time
- Exception details (if failed)
- Retry history
- Full stack trace
Logging in jobs:
// Jobs have access to ILogger
public class NotificationService
{
private readonly ILogger<NotificationService> _logger;
public NotificationService(ILogger<NotificationService> logger)
{
_logger = logger;
}
[JobDisplayName("Send notification to {0}")]
public async Task SendNotificationAsync(string userId)
{
_logger.LogInformation("Starting notification for user {UserId}", userId);
// ... your code
_logger.LogInformation("Notification sent to {UserId}", userId);
}
}
The [JobDisplayName] attribute shows a friendly name in the dashboard.
29.10 Health Check Integration
Add Hangfire to your health checks:
builder.Services.AddHealthChecks()
.AddHangfire(options =>
{
options.MinimumAvailableServers = 1;
options.MaximumJobsFailed = 5;
},
name: "hangfire",
failureStatus: HealthStatus.Degraded,
tags: new[] { "jobs", "hangfire" });
Now your health check endpoint includes Hangfire status.
29.11 Best Practices
✅ Do:
- Enqueue fire-and-forget jobs for non-critical work
- Use delayed jobs for scheduled tasks
- Implement proper error handling (jobs will retry)
- Log job execution with ILogger
- Monitor the Hangfire dashboard regularly
- Use meaningful job names with [JobDisplayName]
- Set appropriate retry counts based on job criticality
❌ Don’t:
- Enqueue jobs in transaction scopes (job might run before transaction commits)
- Rely on Hangfire for critical work (it’s a best-effort background processor)
- Pass large objects as job parameters (serialize what you need)
- Use Hangfire instead of async/await in the request/response cycle
- Forget to configure persistent storage (jobs won’t survive restarts)
- Run long-running operations in the job without checkpoints
29.12 Next Steps
In the next chapter, we’ll implement actual job classes and handle complex scenarios like tracking job status and handling bulk operations.