Reliable Queuing, Scheduling & Retries Across Our .NET Services
Heman Alany | 2026
Real Examples from the MPesa & Maziwa Disbursement Pipeline • Software Engineering
The problems long-running & unreliable work create for request/response APIs.
Core concepts: jobs, storage, servers, queues, and the dashboard.
Step-by-step setup on Windows, Linux, and macOS with Redis.
Windows Services/IIS, Linux systemd, Redis persistence, worker scaling.
Production code from MpesaService — disbursements, C2B, SMS, recon.
Retry design, idempotency, dashboard security, and Prometheus metrics.
An HTTP request thread is the wrong place to execute slow, retry-prone, or mission-critical operations.
A B2C/C2B call can hang or fail mid-flight — the caller shouldn't wait, and we cannot afford to lose the payment record.
If an API process restarts, in-memory retry logic disappears. High-value money movement cannot depend on volatile memory.
A large batch of disbursements shouldn't delay a quick audit log write or an urgent customer SMS error notification.
Engineering and support teams need to see what's queued, what failed, and why — with instant retry capabilities.
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.
BackgroundJob.Enqueue(() => ...)
Runs once, immediately as soon as a worker is free. Our default pattern for C2B/B2C payment processing and file logging.
BackgroundJob.Schedule(() => ..., delay)
Runs once, after a configured timespan delay. Ideal for "retry after 30s" backoff and timed confirmation reminders.
RecurringJob.AddOrUpdate(id, () => ..., cron)
Executes on a cron schedule. Our MpesaScraper runs every 3 seconds to claim and dispatch unsent transactions.
BackgroundJob.ContinueWith(id, () => ...)
Chains multi-step workflows together by triggering secondary tasks as soon as the parent job succeeds.
In our microservices, we configure dedicated Hangfire servers and queues to guarantee that high-frequency background traffic never starves critical payment operations:
B2C disbursement processing & scraper polling
C2B & SMS repayment processing
Dedicated worker for high-value disbursements
WriteLog fire-and-forget file auditing
Centralized error logging & alerting
Manual reconciliation and dispute workflows
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;
});
// 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;
Jobs may execute more than once due to network retries or process crashes. Always verify system state before writing changes.
Pass database entity IDs rather than large serialised object trees to keep Redis memory clean and avoid serialization skew.
Match worker concurrency to downstream system limits (e.g. Safaricom API rate limits, PostgreSQL connection pool size).
Always mount /hangfire behind authenticated filters (IDashboardAuthorizationFilter) with credential management from secure vaults.