Why BackWave
BackWave is a background-job system for .NET in the same species as Hangfire: you enqueue opaque work and it runs once-or-more with retries. What sets it apart is that the job logic is deterministically testable, enqueueing can be transactional, and it runs on infrastructure you already have.
The case for BackWave
Comparisons come further down this page. First, what you actually get when you adopt BackWave, and how it earns the reliability claims it makes.
Test your jobs deterministically
Enqueue a job, advance a virtual clock your test owns, and assert the outcome. Retries, backoff, cron schedules, and dependency releases all run under that clock, so a year of daily cron ticks completes in one call in well under a second, with no sleeps and no real database. The harness pairs the shipping scheduler core with a real in-memory implementation of the same storage contract Postgres passes, not a mock.
Built like infrastructure software
BackWave’s own test suite drives a whole simulated cluster through crashes, clock skew, node isolation, and racing operator actions, and every run replays exactly from a single seed. One conformance suite runs against every supported store on real database instances. Invariant checks stay on in production and fail-stop rather than quietly mis-process work.
Enqueue inside your transaction
Enqueue a job inside your own database transaction: commit and it is durably queued, roll back and it never existed. That closes the dual-write race without an outbox pattern to build or a relay process to babysit, and the same guarantee extends to enqueueing whole workflow graphs.
Guarantees stated honestly
The handler may run more than once, and the docs say so plainly. The recorded outcome and everything downstream of it (terminal state, dependency release, concurrency slot) apply exactly once. Running jobs are held under heartbeat-renewed leases, so a crashed or frozen node simply stops renewing and its peers reclaim the work automatically.
No new infrastructure
The queue is tables in the Postgres, SQL Server, or SQLite you already run, next to your data. There is no broker or Redis to stand up, and no coordinator or leader election either; every node is an interchangeable stateless peer. On Postgres, LISTEN/NOTIFY wakes an idle worker the moment an enqueue commits, giving sub-second pickup with polling underneath as the correctness backstop.
Easy to adopt, hard to outgrow
Put an attribute on a method and the source generator emits the typed payload, reflection-free serialization, and registration. Job identity is a stable Wire Name, so renames never strand in-flight work, and the job path is verified under Native AOT and trimming. Each attempt gets a fresh DI scope, so a scoped DbContext injects exactly as in a controller. If you’re coming from Hangfire, a documented playbook runs both in one process while you port one job at a time.
Operations you can defend
Every dashboard write action (requeue, cancel, pause, trigger) is an individual default-deny callback into your app’s own authorization, and each one lands in an append-only audit trail with the acting identity. Everything an operator can click is also a public API with defined semantics. Health checks tell a transient store blip apart from a genuine halt.
Orchestration for real pipelines
Continuations and dependencies are core and free: a dependent waits until its parent reaches a terminal state, a failed parent cancels the downstream branch, and a job can persist typed output that dependents read race-free. Pro adds a validated fan-out/fan-in DAG builder with atomic graph enqueue and group lifecycle controls.
If Hangfire is the incumbent you’re weighing this against, the next section goes through the differences point by point.
BackWave vs Hangfire
Both are at-least-once background-job systems, and if your handlers are already idempotent for Hangfire, that assumption carries over. The differences are in testability, how enqueue relates to your data, how workers react to new work, how work is orchestrated and isolated, job identity across refactors, and how the dashboard is governed.
| BackWave | Hangfire | |
|---|---|---|
| Testability | A deterministic Virtual-Time simulation drives the scheduling and retry logic with seeded fault injection (orderings, crashes, clock skew), so years of schedule activity replay exactly from a single seed. | Limited determinism. Time-dependent behavior and worker coordination are awkward to exercise deterministically, so retries and scheduling tend to be tested against a real store or not at all. |
| Transactional enqueue | Enqueueing inside your application’s own database transaction is a first-class capability: pass your transaction or DbContext as one argument and the business write and the job commit or roll back together. No outbox pattern to build and maintain. | SQL Server and Postgres storage can enlist job creation into an ambient TransactionScope, so atomic enqueue is possible, but it is opt-in (requires Enlist=true and a TransactionScope), carries the usual distributed-transaction caveats, and is not the default path. |
| Reacting to new work | On Postgres, a committed enqueue fires a LISTEN/NOTIFY wake-up hint so an idle worker picks the job up right away, without polling aggressively just to keep latency low. Polling stays the correctness backstop, so a dropped hint only costs latency, never a lost job. SQL Server falls back to polling; SQLite’s hints are in-process only. | On its SQL storage there is no push signal: workers discover jobs by polling on an interval, so lower latency means polling more often, driving the continuous query load that autoscaling and serverless databases bill for. Redis storage avoids this with blocking pops, but that is the extra infrastructure. |
| Workflows | Continuations and dependencies are core, gated on success or any terminal state, with typed output passing between steps. Pro adds a validated DAG builder with fan-out/fan-in and acyclicity checks. Sagas and compensation are not a built-in primitive; you model them as failure-mode dependencies. | Continuations are core, but fan-out/fan-in Batches are a Hangfire Pro feature. Richer DAGs, sagas, and state machines usually mean reaching for something like Elsa, Temporal, or Durable Functions alongside it. |
| Noisy neighbors | Worker Groups isolate pools by queue, dispatch is either Strict priority or a smooth Weighted round-robin, and per-queue concurrency caps plus pause/resume apply cluster-wide. There is no token-bucket rate limiting or first-class tenant concept. | Workers share pools unless you carefully partition queues, and queue priority is simply the order queues are listed. Per-queue concurrency limits and throttling live in Hangfire Pro. |
| Stable job identity | Jobs carry an explicit, source-generated Wire Name that stays stable across refactors. Renaming a class never strands in-flight work, and there is no reflection over method-call expressions, so it is Native AOT- and trim-safe. | Job identity is derived from method-call expressions via reflection, which couples the wire format to your code shape and works poorly under trimming and Native AOT. |
| Dashboard authorization | The dashboard is interactive, and each Operator Action like requeue or cancel is default-deny, individually audited, and delegated to the host application’s own authorization. BackWave never owns users or roles. | The dashboard is interactive too (operators can requeue, delete, and trigger jobs), but access is coarse: a single authorization filter gates the whole dashboard, so there is no fine-grained, audited, per-action authorization. |
Testability
- BackWave
- A deterministic Virtual-Time simulation drives the scheduling and retry logic with seeded fault injection (orderings, crashes, clock skew), so years of schedule activity replay exactly from a single seed.
- Hangfire
- Limited determinism. Time-dependent behavior and worker coordination are awkward to exercise deterministically, so retries and scheduling tend to be tested against a real store or not at all.
Transactional enqueue
- BackWave
- Enqueueing inside your application’s own database transaction is a first-class capability: pass your transaction or DbContext as one argument and the business write and the job commit or roll back together. No outbox pattern to build and maintain.
- Hangfire
- SQL Server and Postgres storage can enlist job creation into an ambient TransactionScope, so atomic enqueue is possible, but it is opt-in (requires Enlist=true and a TransactionScope), carries the usual distributed-transaction caveats, and is not the default path.
Reacting to new work
- BackWave
- On Postgres, a committed enqueue fires a LISTEN/NOTIFY wake-up hint so an idle worker picks the job up right away, without polling aggressively just to keep latency low. Polling stays the correctness backstop, so a dropped hint only costs latency, never a lost job. SQL Server falls back to polling; SQLite’s hints are in-process only.
- Hangfire
- On its SQL storage there is no push signal: workers discover jobs by polling on an interval, so lower latency means polling more often, driving the continuous query load that autoscaling and serverless databases bill for. Redis storage avoids this with blocking pops, but that is the extra infrastructure.
Workflows
- BackWave
- Continuations and dependencies are core, gated on success or any terminal state, with typed output passing between steps. Pro adds a validated DAG builder with fan-out/fan-in and acyclicity checks. Sagas and compensation are not a built-in primitive; you model them as failure-mode dependencies.
- Hangfire
- Continuations are core, but fan-out/fan-in Batches are a Hangfire Pro feature. Richer DAGs, sagas, and state machines usually mean reaching for something like Elsa, Temporal, or Durable Functions alongside it.
Noisy neighbors
- BackWave
- Worker Groups isolate pools by queue, dispatch is either Strict priority or a smooth Weighted round-robin, and per-queue concurrency caps plus pause/resume apply cluster-wide. There is no token-bucket rate limiting or first-class tenant concept.
- Hangfire
- Workers share pools unless you carefully partition queues, and queue priority is simply the order queues are listed. Per-queue concurrency limits and throttling live in Hangfire Pro.
Stable job identity
- BackWave
- Jobs carry an explicit, source-generated Wire Name that stays stable across refactors. Renaming a class never strands in-flight work, and there is no reflection over method-call expressions, so it is Native AOT- and trim-safe.
- Hangfire
- Job identity is derived from method-call expressions via reflection, which couples the wire format to your code shape and works poorly under trimming and Native AOT.
Dashboard authorization
- BackWave
- The dashboard is interactive, and each Operator Action like requeue or cancel is default-deny, individually audited, and delegated to the host application’s own authorization. BackWave never owns users or roles.
- Hangfire
- The dashboard is interactive too (operators can requeue, delete, and trigger jobs), but access is coarse: a single authorization filter gates the whole dashboard, so there is no fine-grained, audited, per-action authorization.
BackWave vs durable execution (Temporal)
BackWave is a background-job system, not Temporal’s species. It runs a job once-or-more with retries and treats the job body as opaque. It has first-class Workflows (named groups of jobs wired by dependency edges, where a step can read the output of the jobs it depends on), but it is deliberately not a durable-execution engine, and knowing that boundary up front helps you pick the right tool.
Where the line sits (no durable execution)
- No result-driven graphs: output can’t add, skip, or reorder steps.
- No conditionals, signals, waits, or timers inside a job.
- No replay of the steps inside a handler.
What you do get is first-class Workflows: a named, sortable group of jobs connected by static dependency edges, with a graph view and lifecycle controls: cancel-as-a-group and restart-as-new (a full redo that keeps lineage to the original run). A step can pull its dependencies’ job output, so data flows down the graph without any durable-execution machinery. The graph is declared up front and can grow by appending jobs, but never rewrites a running job’s path. Anything past that line is a different product.
Which one do you actually need?
If you need durable workflows (replayable orchestration with step recording, signals and waits, and a graph that reshapes itself on results), you want Temporal, and standing up its cluster is the price of that power.
If you need solid background jobs (enqueue work, run it with retries, on the database you already operate), then BackWave fits, with no new cluster to run.
Ready to look closer?
The documentation covers the core concepts, storage, testing, and the dashboard in depth.
Read the docs