# Background Jobs in .NET with Hangfire

Not every piece of work belongs in the request-response cycle. Sending a confirmation email, generating a report, syncing data with a third-party service, none of that needs to happen before you return a response to the user, and forcing it to happen synchronously just makes your API slower for no real benefit. Hangfire has been my default for background job processing in .NET for exactly this reason: it's simple to set up and handles the operational concerns, persistence, retries, dashboards, that you'd otherwise have to build yourself.

### Fire-and-forget for work that shouldn't block the response

csharp

```csharp
app.MapPost("/api/orders", async (Order order, IOrderService service) =>
{
    await service.CreateAsync(order);
    BackgroundJob.Enqueue<INotificationService>(n => n.SendOrderConfirmationAsync(order.Id));
    return Results.Ok(order);
});
```

The API returns immediately once the order is actually saved. The confirmation email goes out on its own timeline, on its own thread, without the customer waiting on an email provider's response time before they see their order confirmed.

### Delayed jobs for anything that needs to happen later

csharp

```csharp
BackgroundJob.Schedule<ISubscriptionService>(
    s => s.SendRenewalReminderAsync(subscriptionId),
    TimeSpan.FromDays(subscription.DaysUntilExpiry - 7));
```

This replaces what used to require a separate scheduled task or a homegrown polling mechanism, with a single line that Hangfire persists and guarantees will run, even across application restarts.

### Recurring jobs, declared once

csharp

```csharp
RecurringJob.AddOrUpdate<IReportService>(
    "daily-transaction-summary",
    r => r.GenerateDailySummaryAsync(),
    Cron.Daily);
```

I've replaced more than one fragile cron-job-plus-console-app setup with this pattern. It's persisted, visible in the dashboard, and doesn't depend on a separate deployment or a server-level scheduled task that nobody remembers exists until it silently stops running.

### Retries need the same thought as any other resilience concern

By default, Hangfire retries a failed job automatically, which is useful but not automatically safe. A job that partially succeeded before failing, say, it charged a customer but failed to send the confirmation, will retry the entire job, including the part that already succeeded, unless you've made the job idempotent.

csharp

```csharp
public async Task ProcessPaymentAsync(Guid paymentId)
{
    if (await _repository.IsAlreadyProcessedAsync(paymentId)) return;

    await _paymentGateway.ChargeAsync(paymentId);
    await _repository.MarkProcessedAsync(paymentId);
}
```

This is the same idempotency discipline that matters for message consumers in an event-driven system, and it applies here for exactly the same reason: at-least-once execution is the guarantee you actually get, not exactly-once.

### The dashboard is more useful than it looks at first

Hangfire's built-in dashboard shows queued, processing, succeeded, and failed jobs, and it's saved me real debugging time on more than one occasion, being able to see exactly which job failed, how many times, and with what exception, without digging through log files first.

csharp

```csharp
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new HangfireAuthorizationFilter() }
});
```

That authorization filter isn't optional. An unsecured Hangfire dashboard exposes internal job details and lets anyone trigger jobs manually, which is exactly as risky as it sounds.

### Job priorities and queues for mixed workloads

Not every job deserves equal footing. A time-sensitive payment retry shouldn't sit behind a low-priority batch report in the same queue.

csharp

```csharp
[Queue("critical")]
public async Task ProcessRefundAsync(Guid refundId) { }

[Queue("low-priority")]
public async Task GenerateMonthlyReportAsync() { }
```

Separate queues, processed by separate workers if needed, keep a backlog of low-priority work from delaying something time-sensitive.

### Where I draw the line

Hangfire is excellent for jobs measured in seconds to minutes. For genuinely long-running batch processing, or workloads that need horizontal scaling well beyond what a single server's worker pool handles comfortably, I look at dedicated tools built for that scale instead of stretching Hangfire past what it's suited for. Knowing that boundary matters as much as knowing how to use the tool itself.

### The real value

Background jobs let your API stay fast and responsive while the actual work happens on its own schedule. The part that's easy to get wrong isn't the setup, Hangfire makes that simple, it's the idempotency and monitoring discipline around it that determines whether background processing quietly does its job or becomes its own source of confusing, hard-to-reproduce bugs.
