docs/en/Community-Articles/2026-06-22-implementing-background-jobs-with-abp-and-hangfire/Post.md
Background jobs are one of those features that look simple at first and become operationally important very quickly. Sending emails, generating reports, syncing with third-party APIs, cleaning expired data, and processing imports should not block your HTTP requests.
ABP gives you a clean abstraction for background jobs, and Hangfire gives you a production-friendly execution engine with persistence, retries, queues, and a dashboard. The useful part is that you can keep your application code aligned with ABP’s abstractions while swapping in Hangfire as the actual runner.
In this article, I’ll walk through how to implement background jobs with ABP and Hangfire, when to use each piece, and where teams usually get tripped up.
ABP already has a built-in background job system, and it is perfectly fine for simple cases. But it helps to understand what you are trading.
By default, ABP background jobs are:
IBackgroundJobManagerThis is good when:
When you add Volo.Abp.BackgroundJobs.HangFire, ABP can keep the same IBackgroundJobManager programming model, but Hangfire becomes the execution backend.
That gives you:
In practice, Hangfire is the better choice when background processing is part of the actual system design, not just a convenience.
Use ABP default when:
Use Hangfire when:
The nice part of ABP is that your job code does not need to know about Hangfire.
Start with a job arguments class:
public class EmailSendingArgs
{
public string To { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
Then create the job itself:
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection;
public class EmailSendingJob : AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency
{
private readonly IEmailSender _emailSender;
public EmailSendingJob(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public override async Task ExecuteAsync(EmailSendingArgs args)
{
await _emailSender.SendAsync(
args.To,
args.Subject,
args.Body
);
}
}
This job works with ABP’s job abstraction regardless of whether the runtime backend is the default implementation or Hangfire.
To enqueue it:
using System;
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
public class NotificationAppService : ApplicationService
{
private readonly IBackgroundJobManager _backgroundJobManager;
public NotificationAppService(IBackgroundJobManager backgroundJobManager)
{
_backgroundJobManager = backgroundJobManager;
}
public async Task QueueWelcomeEmailAsync(string email)
{
await _backgroundJobManager.EnqueueAsync(
new EmailSendingArgs
{
To = email,
Subject = "Welcome",
Body = "Your account is ready."
},
priority: BackgroundJobPriority.Normal,
delay: TimeSpan.FromMinutes(1)
);
}
}
A few practical notes:
delay is useful for short deferrals and back-office workflows.priority is part of ABP’s abstraction. How it maps operationally depends on the provider.To integrate Hangfire, install the package and wire it into your ABP module.
Using ABP CLI:
abp add-package Volo.Abp.BackgroundJobs.HangFire
Or with NuGet:
Install-Package Volo.Abp.BackgroundJobs.HangFire
Typically this goes into your host module, such as HttpApiHostModule:
using Volo.Abp.BackgroundJobs.Hangfire;
[DependsOn(
typeof(AbpBackgroundJobsHangfireModule)
)]
public class MyProjectHttpApiHostModule : AbpModule
{
}
In ConfigureServices:
using Hangfire;
using Microsoft.Extensions.Configuration;
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddHangfire(config =>
{
config.UseSqlServerStorage(
configuration.GetConnectionString("Default")
);
});
}
If you use PostgreSQL, Redis, or another Hangfire storage provider, configure that instead. The storage decision matters because all servers that process jobs must share the same backing store.
In OnApplicationInitialization:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseAuthentication();
app.UseAuthorization();
app.UseAbpHangfireDashboard();
}
The dashboard middleware should be added after authentication and authorization middleware.
At this point, jobs enqueued through IBackgroundJobManager should use Hangfire as long as the integration is correctly activated.
A common use case is exporting a report that may take several seconds or minutes.
Instead of generating the file during the HTTP request:
public class ReportExportJobArgs
{
public Guid ExportRequestId { get; set; }
public Guid UserId { get; set; }
}
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
public class ReportExportJob : AsyncBackgroundJob<ReportExportJobArgs>, ITransientDependency
{
private readonly IReportExportAppService _reportExportAppService;
public ReportExportJob(IReportExportAppService reportExportAppService)
{
_reportExportAppService = reportExportAppService;
}
public override async Task ExecuteAsync(ReportExportJobArgs args)
{
await _reportExportAppService.GenerateAsync(args.ExportRequestId, args.UserId);
}
}
public async Task<Guid> RequestExportAsync()
{
var exportRequestId = GuidGenerator.Create();
await _backgroundJobManager.EnqueueAsync(
new ReportExportJobArgs
{
ExportRequestId = exportRequestId,
UserId = CurrentUser.GetId()
}
);
return exportRequestId;
}
This pattern scales much better than holding open a web request while doing CPU-heavy or IO-heavy work.
ABP and Hangfire both care about retries, but you should still design jobs carefully.
With ABP background jobs:
A job should be:
For example, sending the same payment capture twice is dangerous. Sending the same “your report is ready” notification twice is annoying but manageable. Design around the difference.
If you use ICancellationTokenProvider, be deliberate. If cancellation means “try again later,” let the exception flow. If cancellation means “stop and do not retry,” return gracefully.
Example:
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.Threading;
public class DataSyncJob : AsyncBackgroundJob<int>
{
private readonly ICancellationTokenProvider _cancellationTokenProvider;
public DataSyncJob(ICancellationTokenProvider cancellationTokenProvider)
{
_cancellationTokenProvider = cancellationTokenProvider;
}
public override async Task ExecuteAsync(int args)
{
var cancellationToken = _cancellationTokenProvider.Token;
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(500, cancellationToken);
}
}
Not every background task is a one-time job.
There are two different patterns:
In ABP, recurring processing is usually modeled with background workers rather than standard background jobs.
Use a worker when you need:
With Hangfire integration, you can derive from HangfireBackgroundWorkerBase and provide a cron expression.
using System.Threading.Tasks;
using Volo.Abp.BackgroundWorkers.Hangfire;
public class ExpiredSessionsCleanupWorker : HangfireBackgroundWorkerBase
{
private readonly ISessionCleanupService _sessionCleanupService;
public ExpiredSessionsCleanupWorker(ISessionCleanupService sessionCleanupService)
{
_sessionCleanupService = sessionCleanupService;
RecurringJobId = "expired-sessions-cleanup";
CronExpression = "0 * * * *";
}
public override async Task DoWorkAsync()
{
await _sessionCleanupService.CleanupAsync();
}
}
A few details matter here:
RecurringJobId should be stable and unique.CronExpression controls the schedule.A simple rule:
Once you run more than one application instance, background processing becomes an architecture concern rather than a coding detail.
If multiple nodes are going to process Hangfire jobs, they must share the same Hangfire storage.
Typical setups include:
Sometimes you want your web app to enqueue jobs but not execute them.
ABP supports this:
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.BackgroundJobs;
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false;
});
}
This is useful when:
If multiple applications share the same Hangfire storage, isolate queues intentionally.
For Hangfire integration in ABP, use AbpHangfireOptions.DefaultQueuePrefix to avoid queue collisions between different applications or environments.
That matters more than teams expect. Without isolation, staging and production can end up looking at the same queues if storage is misconfigured.
Hangfire supports multiple queues, and ABP’s Hangfire integration can route jobs based on conventions or attributes.
In some scenarios, you may want specific jobs to go to specific queues, for example:
emailsexportsintegrationcriticalThis is especially helpful when one queue can become noisy and starve more important work.
The Hangfire dashboard is extremely useful, but it is also an operations surface. Do not expose it casually.
ABP provides authorization support for the dashboard via AbpHangfireAuthorizationFilter.
A typical setup is to:
Example:
app.UseAbpHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[]
{
new AbpHangfireAuthorizationFilter(requiredPermissionName: "Administration.Hangfire")
}
});
Even if your app is internal, treat the dashboard like an admin area:
This is the part that usually saves the most time.
AbpBackgroundJob instead of HangfireIf Hangfire is not properly activated, ABP may continue using its native background job storage and you will see jobs in the AbpBackgroundJob table instead of Hangfire storage.
Check these first:
Volo.Abp.BackgroundJobs.HangFire package is installedAbpBackgroundJobsHangfireModule is added in [DependsOn]AddHangfire(...) is configured correctlyIf any of those are missing, you may think you are using Hangfire while you are actually still on the default provider.
Keep job args small. Prefer identifiers over rich objects.
Good:
OrderIdUserIdExportRequestIdBad:
Retries will happen. If running the same job twice can corrupt data, redesign the workflow.
Common fixes:
Hangfire recurring jobs are cron-based and typically evaluated on minute boundaries. That is fine for most scheduled business work, but it is not a real-time scheduler.
If several apps share one Hangfire store, queue naming and prefixing must be explicit. Otherwise, one application can accidentally process another application's jobs.
For many line-of-business systems, ABP + Hangfire hits a very practical middle ground: easy enough to implement, strong enough to operate.
Before shipping, verify these points:
IBackgroundJobManager unless you explicitly need Hangfire-specific APIsIBackgroundJobManager for most jobs so your application code stays provider-independent.AbpBackgroundJob, your Hangfire integration is probably not fully activated.