Skip to content

ADR-0003 — Hangfire on PostgreSQL

  • Status: Accepted
  • Date: 2026-04-28

Context

Sense Worker needs:

  • Recurring jobs (partman maintenance, access expiration, PMTiles generation, Citylens migration trigger).
  • One-off background jobs (manually triggered or operation-emitted).
  • A dashboard for ops to see what's running and re-trigger failed jobs.
  • Operability — restart-safe, retry on failure, distributed-safe (multiple Worker pods don't double-execute the same job).

Options considered:

  1. Hangfire on Postgres — battle-tested .NET scheduler; uses Postgres for state.
  2. Quartz.NET on Postgres / SQL Server / Redis — capable but heavier ergonomics; less of a turn-key dashboard.
  3. Temporal — the right answer for complex workflows with branching state. Overkill for our cron-style jobs and adds another stateful service to operate.
  4. Kubernetes CronJobs — works for purely cron-driven recurring jobs, but doesn't handle one-offs from the application code or give us a dashboard.

Decision

Use Hangfire 1.8 with Hangfire.PostgreSql 1.21, backed by a separate Postgres database (axion_sense_tasks) on the same Postgres cluster as the main DB.

Consequences

  • Positive: Zero new infrastructure. Postgres is already there.
  • Positive: Hangfire Dashboard (gated by OIDC) gives ops a one-stop view of recurring + one-off jobs and a "retrigger" button.
  • Positive: Distributed by default — multiple Worker pods can run; Hangfire claims jobs atomically.
  • Positive: Job state survives Worker restarts.
  • Negative: Postgres takes the load of Hangfire's polling + state churn. Mitigated by isolating it in its own DB so backups, monitoring, and grants are scoped separately.
  • Negative: Hangfire's data model is a bit chatty (high write rate to a small set of tables). Run VACUUM regularly; the Hangfire.PostgreSql package handles partitioning of the queue tables.
  • Negative: Hangfire is not a workflow engine. If a job grows into a multi-step workflow with state and branching, that's the moment to revisit Temporal.

Notes

The axion_sense_tasks DB also holds worker-only domain tables (CitylensTrackMapping, the migration completion flag). Keeping all "worker-facing state" in one DB simplifies the ownership model — the Worker process is the only writer.