Skip to content

Hangfire

Developer UtilitiesScheduler/Job ManagementC#

What it is

Hangfire runs background jobs in .NET with persistent storage, automatic retries, scheduling and a web dashboard — no separate service required.

Enqueue fire-and-forget jobs, schedule delayed ones, define recurring jobs with cron expressions, and chain continuations. State lives in the database.

Installation

dotnet add package Hangfire

Getting started

The smallest useful thing you can do with it, and what each part means.

The four job types
// Fire and forget — runs once, as soon as a worker is free.
BackgroundJob.Enqueue<IEmailService>(x => x.SendWelcome(userId));

// Delayed.
BackgroundJob.Schedule<IEmailService>(x => x.SendReminder(userId), TimeSpan.FromDays(3));

// Recurring, by cron.
RecurringJob.AddOrUpdate<IReportService>(
    "nightly-report", x => x.GenerateAsync(null!), "0 3 * * *");

// Continuation — runs only after the parent succeeds.
var id = BackgroundJob.Enqueue<IImportService>(x => x.Import(fileId));
BackgroundJob.ContinueJobWith<INotifyService>(id, x => x.Notify(fileId));
Arguments are serialised into the database, so they must be simple and serialisable — passing an entity or a delegate will fail at enqueue time.

Advanced usage

Where the library earns its place over a simpler alternative.

Retries, concurrency limits and the dashboard
[AutomaticRetry(Attempts = 5, DelaysInSeconds = new[] { 60, 300, 900 })]
[DisableConcurrentExecution(timeoutInSeconds: 300)]
public async Task ProcessAsync(int orderId)
{
    // The job must be idempotent: a retry may run it again after
    // partial completion.
    if (await _orders.IsProcessedAsync(orderId)) return;
    await _orders.ProcessAsync(orderId);
}

// Secure the dashboard — it exposes job data and allows re-execution.
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new AdminOnlyFilter() }
});
Two things people get wrong: jobs must be idempotent because retries are automatic, and the dashboard is unauthenticated by default in some setups — it must be locked down.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Job failed with a serialisation exception
An argument cannot be serialised. Pass an id and load the object inside the job.
Recurring jobs run on every instance
That is expected — all servers poll the same storage. Hangfire coordinates, but use DisableConcurrentExecution for extra safety.

Best practices

  • Write idempotent jobs; automatic retries mean a job can run more than once.
  • Pass identifiers rather than objects — arguments are serialised into storage.
  • Always secure the dashboard; it can trigger and inspect jobs.
  • Use DisableConcurrentExecution for jobs that must not overlap across instances.

Background

Why it exists, and what it was reacting to.

Hangfire's distinguishing choice is storing jobs in your existing database, so a job survives an application restart without deploying Redis or a message broker.