Background Jobs
This guide covers background processing in Wallow using Hangfire for scheduled and recurring jobs, alongside Wolverine for event-driven processing.
Overview
Wallow uses Hangfire for scheduled and recurring background jobs, backed by PostgreSQL. Hangfire provides persistent job storage, a dashboard UI, automatic retries, cron scheduling, and fire-and-forget execution.
Use Hangfire for time-based work: scheduled tasks, recurring cron jobs, deferred execution, and jobs not triggered by events.
Use Wolverine for event-driven work: reacting to domain events, cross-module integration events, immediate command processing, and saga orchestration.
Hangfire Configuration
Service Registration
Hangfire is configured via AddHangfireServices in api/src/Wallow.Api/Extensions/HangfireExtensions.cs. It connects to PostgreSQL using the DefaultConnection connection string and stores jobs in the hangfire schema.
The Hangfire server is registered with shutdown/stop timeouts for graceful termination during deployments.
Dashboard
The Hangfire dashboard is available at /hangfire, protected by HangfireDashboardAuthFilter (api/src/Wallow.Api/Middleware/HangfireDashboardAuthFilter.cs). The filter requires an authenticated user holding the AdminAccess permission — it checks User.GetPermissions(), not a role claim, so a user whose roles expand to AdminAccess passes and a user merely named admin does not.
Anonymous access is not environment-driven. It is the Hangfire:AllowAnonymousDashboard configuration flag, read in UseHangfireDashboard and passed to the filter's constructor. The filter's own comment explains why: a configuration flag means no environment name can turn the dashboard open by accident. Local development sees an open dashboard only because api/src/Wallow.Api/appsettings.Development.json sets that flag to true.
| Configuration | URL | Access |
|---|---|---|
Hangfire:AllowAnonymousDashboard: true (the shipped Development default) |
http://localhost:5001/hangfire | Open to all |
Flag absent or false (every other environment) |
https://your-domain/hangfire | AdminAccess permission required |
IJobScheduler Abstraction
Wallow provides an IJobScheduler abstraction in api/src/Shared/Wallow.Shared.Kernel/BackgroundJobs/IJobScheduler.cs for enqueuing and scheduling jobs without depending on Hangfire directly. The Hangfire implementation lives in api/src/Shared/Wallow.Shared.Infrastructure.BackgroundJobs/HangfireJobScheduler.cs.
Recurring Jobs
Recurring jobs are registered at startup in Program.cs using IRecurringJobManager via a scoped DI call:
| Job | Schedule | Location |
|---|---|---|
SystemHeartbeatJob |
Every 5 minutes | api/src/Wallow.Api/Jobs/SystemHeartbeatJob.cs |
RetryFailedEmailsJob |
Every 5 minutes (feature-flagged) | api/src/Modules/Notifications/Wallow.Notifications.Infrastructure/Jobs/RetryFailedEmailsJob.cs |
OpenIddictTokenPruningJob |
Every 4 hours | api/src/Modules/Identity/Wallow.Identity.Infrastructure/Jobs/OpenIddictTokenPruningJob.cs |
ExpiredInvitationPruningJob |
Every hour | api/src/Modules/Identity/Wallow.Identity.Infrastructure/Jobs/ExpiredInvitationPruningJob.cs |
SessionPruningJob |
Daily (Cron.Daily()) |
api/src/Modules/Identity/Wallow.Identity.Infrastructure/Jobs/SessionPruningJob.cs |
OrphanedObjectSweepJob |
Daily (Cron.Daily(), feature-flagged) |
api/src/Modules/Storage/Wallow.Storage.Infrastructure/Jobs/OrphanedObjectSweepJob.cs |
The RetryFailedEmailsJob and OrphanedObjectSweepJob are conditionally registered behind their
modules' feature flags (Modules.Notifications and Modules.Storage). The orphan sweep deletes
storage-backend objects under the tenant- key prefix that no StoredFile row references and
that are older than 24 hours — the residue of an upload whose backend write succeeded but whose
database commit failed. The age threshold keeps it clear of in-flight uploads (presigned upload
URLs expire in minutes).
Cron Expression Reference
| Expression | Description |
|---|---|
*/5 * * * * |
Every 5 minutes |
0 * * * * |
Every hour |
0 */4 * * * |
Every 4 hours |
0 2 * * * |
Daily at 2 AM |
0 0 * * 0 |
Weekly on Sunday at midnight |
0 0 1 * * |
Monthly on the 1st at midnight |
Hangfire also provides helpers: Cron.Minutely, Cron.Hourly, Cron.Daily, Cron.Weekly, Cron.Monthly, Cron.Yearly, and overloads like Cron.Daily(hour: 3).
Job Patterns
Job Class Structure
Jobs use constructor injection, an ExecuteAsync method, and structured logging via [LoggerMessage] source generators. The SessionPruningJob is a representative example: it takes its DbContext, a TimeProvider, and an ILogger<T> through the primary constructor, queries for revoked or expired ActiveSession rows, removes them in a single SaveChangesAsync, and returns the number of rows pruned.
Error Handling
Use [AutomaticRetry] to configure retry behavior. The default is 10 retries with exponential backoff. Set Attempts = 0 to disable retries or Attempts = 3 for a limited count.
Per-item error handling within batch jobs prevents a single failure from aborting the entire run.
Job Parameters
Job parameters are serialized to JSON and stored in PostgreSQL. Pass simple types (IDs, strings) rather than complex objects. Retrieve full entities from the database inside the job.
Best Practices
- Idempotency: Jobs may run multiple times due to retries or server restarts. Check state before processing.
- Cancellation: Check
CancellationTokenregularly in long-running jobs. - Tenant context: For tenant-scoped jobs, use
ITenantContextFactory.CreateScope(tenantId)to establish the tenant context before executing queries. - Batching: Process large datasets in batches to control memory usage.
Wolverine vs Hangfire
| Scenario | Use |
|---|---|
| React to domain event | Wolverine |
| Scheduled task (cron) | Hangfire |
| Cross-module notification | Wolverine |
| Recurring maintenance | Hangfire |
| Long-running workflow/saga | Wolverine |
| Deferred execution (run in X minutes) | Hangfire |
The two systems complement each other. A Hangfire job can publish a Wolverine event upon completion, bridging time-based triggers with event-driven processing.
Related Documentation
- Messaging — the Wolverine half of the table above
- Authorization — where the
AdminAccesspermission the dashboard requires comes from - Module Creation — adding a job to a new module
- Observability — job telemetry and dashboards