Research & Development Blog

Hangfire for Background Processing

Reliable Queuing, Scheduling & Retries Across Our .NET Services

Heman Alany | 2026

Real Examples from the MPesa & Maziwa Disbursement Pipeline • Software Engineering

Session Agenda

01

Why We Need Background Jobs

The problems long-running & unreliable work create for request/response APIs.

02

What Hangfire Is & How It Works

Core concepts: jobs, storage, servers, queues, and the dashboard.

03

Installing & Running Locally

Step-by-step setup on Windows, Linux, and macOS with Redis.

04

Deploying to Our Servers

Windows Services/IIS, Linux systemd, Redis persistence, worker scaling.

05

How We Actually Use It

Production code from MpesaService — disbursements, C2B, SMS, recon.

06

Patterns, Pitfalls & What's Next

Retry design, idempotency, dashboard security, and Prometheus metrics.

1. The Problem Before Background Jobs

An HTTP request thread is the wrong place to execute slow, retry-prone, or mission-critical operations.

Safaricom is Slow or Times Out

A B2C/C2B call can hang or fail mid-flight — the caller shouldn't wait, and we cannot afford to lose the payment record.

Retries Need to Survive a Crash

If an API process restarts, in-memory retry logic disappears. High-value money movement cannot depend on volatile memory.

One Slow Job Shouldn't Block Another

A large batch of disbursements shouldn't delay a quick audit log write or an urgent customer SMS error notification.

We Need Visibility & Control

Engineering and support teams need to see what's queued, what failed, and why — with instant retry capabilities.

2. What is Hangfire?

Hangfire is an open-source .NET library that allows you to create, persist, and process background jobs reliably — without requiring separate standalone services or complex external broker management for standard workflows.

  • Persistent Storage: Jobs are persisted to Redis — guaranteeing they survive application restarts or server crashes.
  • In-Process Execution: Runs seamlessly inside any .NET application (ASP.NET Core API, Console Apps, Windows Services).
  • Built-in Dashboard: Ships with a real-time web dashboard for queue health inspection, metrics, and manual 1-click retries.
  • Horizontal Scalability: Scale out worker instances by pointing multiple servers at the same shared storage.

3. Four Kinds of Jobs in Hangfire

1. Fire-and-Forget

BackgroundJob.Enqueue(() => ...)

Runs once, immediately as soon as a worker is free. Our default pattern for C2B/B2C payment processing and file logging.

2. Delayed

BackgroundJob.Schedule(() => ..., delay)

Runs once, after a configured timespan delay. Ideal for "retry after 30s" backoff and timed confirmation reminders.

3. Recurring

RecurringJob.AddOrUpdate(id, () => ..., cron)

Executes on a cron schedule. Our MpesaScraper runs every 3 seconds to claim and dispatch unsent transactions.

4. Continuations

BackgroundJob.ContinueWith(id, () => ...)

Chains multi-step workflows together by triggering secondary tasks as soon as the parent job succeeds.

4. Architecture & Queue Isolation

In our microservices, we configure dedicated Hangfire servers and queues to guarantee that high-frequency background traffic never starves critical payment operations:

mpesa-queue

B2C disbursement processing & scraper polling

mpesa-repayment-queue

C2B & SMS repayment processing

disbursement-mpesa-queue

Dedicated worker for high-value disbursements

log-queue

WriteLog fire-and-forget file auditing

error-queue

Centralized error logging & alerting

manual-recon

Manual reconciliation and dispute workflows

5. Production Implementation Examples

Program.cs Configuration with Redis

var redisConn = ConnectionMultiplexer.Connect(redisConnectionString);

builder.Services.AddHangfire(cfg => cfg
    .UseRedisStorage(redisConn, new RedisStorageOptions {
        Prefix = "OPO-MpesaService:"
    }));

builder.Services.AddHangfireServer(o => {
    o.Queues = new[] { "mpesa-queue" };
    o.WorkerCount = 1;
});

Recurring Worker with Safe Row Claiming (FOR UPDATE SKIP LOCKED)

// Registered in ApplicationStarted to ensure Redis connectivity
app.Lifetime.ApplicationStarted.Register(() =>
{
    RecurringJob.AddOrUpdate<MpesaScraper>(
        "MpesaScraper",
        x => x.ScrapeUnsent(),
        "*/3 * * * * *" // Polling every 3 seconds
    );
});

// SQL query for non-blocking concurrent worker polling:
SELECT * FROM "disbursement"
WHERE "issent" = false AND "systemretries" < 3
ORDER BY "id" LIMIT 20
FOR UPDATE SKIP LOCKED;

6. Best Practices & Key Takeaways

Make Jobs Idempotent:

Jobs may execute more than once due to network retries or process crashes. Always verify system state before writing changes.

Keep Job Arguments Minimal:

Pass database entity IDs rather than large serialised object trees to keep Redis memory clean and avoid serialization skew.

Size WorkerCount Deliberately:

Match worker concurrency to downstream system limits (e.g. Safaricom API rate limits, PostgreSQL connection pool size).

Secure the Dashboard:

Always mount /hangfire behind authenticated filters (IDashboardAuthorizationFilter) with credential management from secure vaults.