Introduction
In modern web applications, not all operations should run immediately or block user interactions. Tasks such as sending emails, generating reports, syncing data, or running scheduled cleanups should be handled asynchronously in the background.
In ASP.NET Core, this is where background job frameworks like Hangfire and Quartz.NET shine. They help execute long-running or scheduled tasks reliably, without freezing the main thread or affecting API performance.
This article will guide you through practical implementation of background task processing in ASP.NET Core using both Hangfire and Quartz.NET, explaining how to choose the right tool for your use case.
1. Why Use Background Processing?
Before jumping into implementation, let’s understand the need for background jobs in web systems.
Common Scenarios
Sending emails, notifications, or invoices.
Generating PDF or Excel reports asynchronously.
Database cleanup or data synchronization jobs.
Queue processing (import/export operations).
Scheduled maintenance tasks (daily backups, archiving).
If done synchronously, these operations can delay API responses or even cause timeouts. By moving them to background workers, the main API remains responsive and scalable.
2. Choosing Between Hangfire and Quartz.NET
| Feature | Hangfire | Quartz.NET |
|---|
| Job Type | Fire-and-forget, recurring, delayed | Cron-based, triggered, recurring |
| Storage | SQL Server, Redis, etc. | Database-backed (various providers) |
| Dashboard | Built-in web UI | No built-in dashboard |
| Ease of Use | Very simple | More configuration-heavy |
| Best For | APIs, Web Apps | Enterprise-level schedulers |
Both frameworks are reliable, but Hangfire is more developer-friendly for everyday background jobs, while Quartz.NET is better suited for complex scheduling workflows.
3. Architecture Overview
Technical Workflow
+-------------+ +----------------------+ +------------------+
| ASP.NET API | -----> | Background Framework | -----> | Task Execution |
| (Job Enqueue)| | (Hangfire/Quartz) | | (Email, Report, etc.) |
+-------------+ +----------------------+ +------------------+
Flow Explanation
The API receives a request.
The background framework queues the task.
The worker process executes the job asynchronously.
The main API thread remains free to serve other requests.
4. Implementing Background Tasks with Hangfire
Step 1: Install Packages
dotnet add package Hangfire
dotnet add package Hangfire.SqlServer
Step 2: Configure Hangfire in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHangfire(config =>
config.UseSqlServerStorage(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddHangfireServer();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHangfireDashboard("/hangfire");
app.MapControllers();
app.Run();
This registers Hangfire and enables its built-in dashboard at /hangfire.
Step 3: Create a Background Job Service
public interface IEmailService
{
Task SendWelcomeEmailAsync(string email);
}
public class EmailService : IEmailService
{
public async Task SendWelcomeEmailAsync(string email)
{
await Task.Delay(2000); // Simulate sending email
Console.WriteLine($"Email sent to {email}");
}
}
Step 4: Enqueue a Background Job
[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobs;
private readonly IEmailService _emailService;
public AccountController(IBackgroundJobClient backgroundJobs, IEmailService emailService)
{
_backgroundJobs = backgroundJobs;
_emailService = emailService;
}
[HttpPost("register")]
public IActionResult RegisterUser(string email)
{
// Save user logic here
_backgroundJobs.Enqueue(() => _emailService.SendWelcomeEmailAsync(email));
return Ok("User registered. Welcome email scheduled.");
}
}
Now, when a new user registers, an email task is queued and executed in the background.
Step 5: Schedule and Recurring Jobs
// Run once after delay
BackgroundJob.Schedule(() => Console.WriteLine("Job executed after 5 minutes"), TimeSpan.FromMinutes(5));
// Run recurring job
RecurringJob.AddOrUpdate("daily-job", () => Console.WriteLine("Daily cleanup task"), Cron.Daily);
Step 6: Hangfire Dashboard
Visit /hangfire in your browser to view:
Queued jobs
Completed jobs
Failed jobs
Retry options
5. Implementing Background Tasks with Quartz.NET
Step 1: Install Packages
dotnet add package Quartz
dotnet add package Quartz.Extensions.Hosting
Step 2: Configure Quartz in Program.cs
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
});
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Step 3: Create a Job
public class DataCleanupJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
Console.WriteLine($"Data cleanup executed at {DateTime.Now}");
return Task.CompletedTask;
}
}
Step 4: Schedule the Job
public class QuartzJobScheduler
{
public static async Task ScheduleJobs(ISchedulerFactory schedulerFactory)
{
var scheduler = await schedulerFactory.GetScheduler();
var job = JobBuilder.Create<DataCleanupJob>()
.WithIdentity("DataCleanupJob")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("DailyTrigger")
.StartNow()
.WithCronSchedule("0 0 1 * * ?") // Runs every day at 1 AM
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
Call this method in Program.cs after service registration:
var schedulerFactory = builder.Services.BuildServiceProvider().GetRequiredService<ISchedulerFactory>();
await QuartzJobScheduler.ScheduleJobs(schedulerFactory);
6. Comparing Hangfire and Quartz.NET
| Feature | Hangfire | Quartz.NET |
|---|
| Ease of Setup | Very easy | Slightly complex |
| Dashboard | Built-in web dashboard | External integration required |
| Persistence | SQL, Redis, etc. | Database-backed |
| Job Types | Background, recurring, delayed | Cron-based, advanced triggers |
| Use Case | Web apps, microservices | Complex enterprise schedulers |
Choose Hangfire when:
Choose Quartz.NET when:
You need precise scheduling with cron expressions.
You require job chaining, triggers, or dependency workflows.
7. Best Practices
Use Persistence: Always configure persistent storage (SQL/Redis) to retain job history.
Retry Mechanism: Hangfire automatically retries failed jobs; handle exceptions in Quartz manually.
Logging and Monitoring: Use Serilog or Application Insights for tracking job execution.
Security: Protect dashboards like /hangfire with authentication.
Avoid Heavy Logic in Controllers: Offload data-intensive operations to background workers.
8. Real-World Example – Invoice Processing
When a user requests an invoice generation:
API saves request to DB.
Hangfire job is enqueued to process and email the PDF.
Job runs asynchronously, updates status, and notifies the user.
This design ensures API responsiveness and scalable background operations.
Conclusion
Background task processing is essential for building responsive, fault-tolerant, and scalable web applications.
Hangfire offers simplicity, reliability, and a visual dashboard — perfect for most web apps.
Quartz.NET provides deep scheduling flexibility and fine-grained control — ideal for enterprise systems.
By integrating one of these frameworks, your ASP.NET Core applications can efficiently handle long-running, recurring, and scheduled tasks, improving both user experience and system performance.