# BackWave vs TickerQ

> TickerQ is an excellent scheduler with a permissive license and a store for every taste. Pick BackWave when a duplicated or lost job costs real money, and when a hard node crash has to heal itself without adding Redis.

Competitor claims verified on 15 August 2026.

Both libraries reject the old .NET pattern of reflection and runtime type lookup. Both use a source
generator, both run on Native AOT, and both ship a free dashboard. If you have a Hangfire habit, both
feel like an upgrade.

The two products then diverge on what they optimize for.

**TickerQ optimizes for reach and immediacy.** It is MIT and Apache dual-licensed, it has 3.6k GitHub
stars and a Discord, and it stores its work almost anywhere: EF Core over four relational engines,
Redis, or MongoDB. It schedules across separate applications through TickerQ Hub. You can create and
edit a scheduled job from the dashboard.

**BackWave optimizes for what happens when things go wrong.** It starts the same way TickerQ does, on
an in-memory store with no database and no configuration, and then it takes the unhappy path
seriously. A node that dies mid-job has its work reclaimed when the lease expires, on every storage
adapter, and a node that already lost the job cannot write an outcome for it afterward. Reliability is
something to demonstrate rather than claim, so the scheduler is held to a deterministic simulator that
replays failures on demand, and every adapter is held to one conformance suite that runs against a
real database. That same test machinery ships to you, so your own jobs run under virtual time with no
database and no waiting.

| Question | BackWave | TickerQ |
|---|---|---|
| A node dies mid-job. What reclaims the work? | Lease expiry, in every adapter | The Redis package only, see [crash recovery](#crash-recovery-the-largest-difference) |
| A slow node writes an outcome after it lost the job | Fenced out and discarded | Not fenced |
| License | Source available, no-compete | MIT / Apache 2.0 |
| Persistence | Postgres, SQL Server, SQLite, in-memory | EF Core (4 engines), Redis, MongoDB, in-memory |
| Named queues and fair dispatch | Yes, Strict and Weighted | No, priority per function |
| Cluster-wide concurrency cap | Yes, per queue | No, `MaxConcurrency` is per node |
| Deterministic simulation of the scheduler | Yes | No |
| Test your jobs over virtual time | Yes, `BackWave.Testing` | No |
| OpenTelemetry | Traces, metrics, logs on `messaging.*` / `db.*` | Traces and logs on custom `tickerq.*` tags |
| Create and edit a job from the dashboard | No, by design | Yes |
| Cross-application scheduling | No | Yes, TickerQ Hub |

## What each product is

**TickerQ** is a scheduler-first job library. Its two units are the TimeTicker, a one-shot job at a
point in time, and the CronTicker, a cron expression that produces occurrence rows. You mark a method
with `[TickerFunction]`, and the source generator registers it. The scheduler computes the next due
time, sleeps until then, and runs the due functions on its own task scheduler with four priorities.

**BackWave** is a background-job system built around a determinism boundary. The core holds every
decision: due calculation, retries, lease timeout, state transitions. The core performs no I/O and
never reads the wall clock. The shell is the per-node loop that runs the core's commands through the
storage contract. Everything above the storage contract is simulable. Everything below it is an
adapter that the conformance suite verifies against a real database.

That split is the reason for most of the differences in this report. It is also a cost. BackWave has
more concepts than TickerQ because the boundary forces them to be named.

## Shared ground

These points are genuinely equal. Neither product wins here.

- **Compile-time job definition.** Both use a source generator. Neither scans assemblies at startup.
- **No runtime reflection.** Both emit the registry and the invocation path at compile time.
- **Native AOT.** Both support it.
- **A free dashboard.** Both ship one, and neither charges for it.
- **Cron scheduling with time zones.**
- **A zero-configuration start.** Both run on an in-memory store with no database, which is the fast
  way to try either one.
- **Retries with a backoff.** TickerQ sets `Retries` and `RetryIntervals` on each ticker it creates.
  BackWave sets a baseline per worker group and overrides it per job type with
  `[Retry(3, 1, 5)]`, validated by the source generator.
- **Cooperative cancellation** through a `CancellationToken`.
- **Dependency injection** that resolves your handler's services.
- **EF Core integration.** TickerQ persists through EF Core. BackWave uses EF Core for transactional
  enqueue alongside your own `SaveChanges`.

If your requirement list stops here, either product serves you, and TickerQ costs less to adopt
because of its license and the number of stores it runs on.

## Defining a job

The two APIs are close in spirit.

TickerQ marks a method and names the function:

```csharp
public class InvoiceJobs
{
    [TickerFunction("send-invoice")]
    public Task SendInvoiceAsync(TickerFunctionContext<InvoicePayload> context, CancellationToken ct)
        => gateway.SendAsync(context.Request.OrderId, ct);
}
```

BackWave marks a method and names the wire format:

```csharp
public sealed class InvoiceJobs(IInvoiceGateway gateway)
{
    [Job("send-invoice", Queue = "billing")]
    public Task SendInvoiceAsync(string orderId, JobContext context, CancellationToken ct)
        => gateway.SendAsync(orderId, ct);
}
```

Two differences matter.

**The payload.** TickerQ carries a single request object that you serialize into the ticker. BackWave
generates a payload record from the method parameters, so a job with five arguments stays a typed
signature and needs no hand-written wrapper type.

**The stored identity.** BackWave calls `"send-invoice"` the Wire Name. It is mandatory, and it is
never derived from a CLR type name, so a class rename never breaks a stored job. BackWave also ships a
job manifest: a committed snapshot of every registered Wire Name that a test helper verifies. A
wire-format change then shows up in a pull-request diff. TickerQ's function name is also an explicit
string and is stable in the same way, but TickerQ ships no manifest check.

**Unroutable jobs.** BackWave has a terminal state named Quarantined for a stored job whose Wire Name
has no registered handler, or whose payload no longer deserializes. It is loud and visible, and it is
deliberately distinct from Dead-Lettered, which means the job ran and kept failing. TickerQ does not
separate these two cases.

## Crash recovery, the largest difference

This is the section that decides most production choices, so it is stated with source references.

### How BackWave reclaims work

A worker holds a lease: a time-bounded, heartbeat-renewed claim on a job. When the lease expires, the
job is claimable again. This is the mechanism behind BackWave's at-least-once contract. A crash, a
hung process, a paused container, and a lost network route all produce the same outcome. The lease
lapses and another node picks the job up. Lease expiry counts as an attempt, exactly like a thrown
exception. This behavior lives in every storage adapter, because the storage contract requires it, and
the conformance suite proves it against the real database.

BackWave then adds a second guarantee on top, called Effect-Once. The handler body can run more than
once, and idempotency is still your problem. What runs exactly once is the *recorded outcome* of an
attempt and every state change that flows from it: the terminal state, the dependency latch decrement,
the concurrency-limit slot release. Storage enforces that with a fence: every outcome write carries
the identity of the worker and the attempt it belongs to, and storage refuses a write whose identity
no longer matches the current holder. A node isolated past its lease expiry writes its stale outcome
into nothing, and because every downstream effect is a consequence of that one write, fencing the
write fences everything below it. The simulator carries a fault for exactly this case: it cuts a node
off from storage while the node keeps running its handler and keeps believing that it holds the lease,
then asserts on heal that the stale write changed nothing.

### How TickerQ reclaims work

TickerQ's EF Core provider claims a row with `ExecuteUpdateAsync`. It sets `LockHolder`, `LockedAt`,
and `Status = InProgress`, then re-reads the rows filtered by its own `LockHolder`. The predicate that
selects claimable rows is in `TickerQueryExtensions.cs`:

```csharp
Expression<Func<TTimeTicker, bool>> pred = e =>
    ((e.Status == TickerStatus.Idle || e.Status == TickerStatus.Queued) && e.LockHolder == lockHolder) ||
    ((e.Status == TickerStatus.Idle || e.Status == TickerStatus.Queued) && e.LockedAt == null);
```

Read the second clause. A row is claimable when `LockedAt` is null. There is no comparison against a
lease deadline, and there is no time term at all. A row whose `LockedAt` is set is not claimable, no
matter how old that timestamp is.

TickerQ does release its rows on the paths it controls. Its scheduler background service releases
acquired resources on a graceful shutdown, on a scheduler restart, and in its top-level exception
handler. A normal deploy is therefore safe.

The gap is the path that TickerQ does not control. A hard kill, an out-of-memory kill, a node that
loses power, or a hung process leaves rows with `Status = InProgress` and a non-null `LockedAt`.
Nothing in the EF Core provider reclaims them on a timer.

TickerQ does solve this, in one package. A node heartbeat service writes an `hb:<node>` key to Redis
with a TTL of the heartbeat interval plus 20 seconds. When a key expires, a Lua script releases the
dead node's resources. That service lives in `TickerQ.Caching.StackExchangeRedis`. It is not part of
the EF Core persistence package.

### The practical statement

| Deployment | TickerQ behavior after a hard node crash |
|---|---|
| EF Core, single node | The row stays `InProgress` until you intervene |
| EF Core, multiple nodes, no Redis | The row stays `InProgress` until you intervene |
| EF Core plus the Redis caching package | The dead node's rows are released after the heartbeat TTL |

BackWave's answer is the same in all three rows: the lease expires and the job runs again.

This is a real difference in operational risk, and it is not a difference in marketing. If you run
TickerQ in a cluster, add the Redis package. That is the fair recommendation, and TickerQ's own
architecture supports it well.

## Queues, priority, and concurrency

**TickerQ.** Priority is a property of the *function*, declared once on `[TickerFunction]`, with four
levels: `LongRunning`, `High`, `Normal`, and `Low`. The scheduler orders the due functions by priority
and queues them onto its own task scheduler. `MaxConcurrency` caps parallel jobs and defaults to
`Environment.ProcessorCount`. `LongRunning` jobs are excluded from that cap. Groups organize functions
for the dashboard and for shared configuration.

Two limits follow from this design. Priority is fixed per function, so the same function cannot be
urgent for one caller and background for another. `MaxConcurrency` is a per-node semaphore, so ten
nodes with a cap of 16 run up to 160 jobs at once.

**BackWave.** A job belongs to exactly one named queue, declared on the job type and overridable at
enqueue. Priority is never a property of a job. It lives in the consumer's dispatch policy:

- **Strict** takes an ordered list of queues and accepts starvation of the lower ones.
- **Weighted** uses smooth weighted round-robin, with no randomness, so a 6:1 weight gives the low
  queue a real share.

Both policies are work-conserving. A worker never idles while a served queue has due work.

BackWave then separates two caps that are easy to confuse:

- The **concurrency limit** is per queue and cluster-wide, enforced at claim time. A slot is released
  on a terminal state or on lease expiry, so a crash never leaks one. Ten nodes with a limit of 16 run
  16 jobs at once, not 160.
- **Backpressure** is node-local. A node stops claiming when its worker pool has no free slot, so a
  claim never exceeds free capacity.

The two gates are independent. A node can be backpressured while the cluster limit has free slots, or
limit-saturated while its own pool is idle.

If you have ever needed "never more than 4 of these running against the payment provider, across the
whole fleet," that requirement is a one-line BackWave configuration and has no TickerQ equivalent.

BackWave also separates a worker from a pump. A worker is one execution slot. A pump is one
independent claim, dispatch, and report loop with its own store round-trips. Adding pumps multiplies
store-I/O parallelism at the cost of a few more connections.

## Recurring schedules

TickerQ's CronTicker produces occurrence rows in advance. It seeds occurrences at startup and it
supports `SkipIfAlreadyRunning()` to avoid an overlap. TickerQ also offers an opt-in pass, gated on a
staleness threshold, that discards occurrences that went stale while the system was down.

BackWave's recurring schedule is a template that mints jobs as time passes. The schedule and the jobs
it mints are distinct objects with distinct lifecycles. Two per-schedule policies exist:

- **Catch-up.** `Skip` is the default, so a missed occurrence stays missed. `Coalesce` mints exactly
  one make-up job. Replaying every missed occurrence is deliberately unsupported, because a system
  that was down for a day must not wake up and run 1,440 copies of a minutely job.
- **No-overlap.** Opt-in. A new instance is not minted while a previous one is non-terminal. The
  skipped tick is recorded visibly, never silently.

The features overlap heavily. The difference is in how each product proves the behavior. BackWave's
core reads a virtual clock, so a test advances three years of schedule activity in milliseconds and
asserts the exact set of minted jobs. That test never flakes, because the same seed always produces
the same run.

## Correctness engineering

This is BackWave's core investment, and TickerQ has no comparable layer. That is a design choice on
TickerQ's part, not a defect, and TickerQ's simpler architecture is what makes it easy to read.

BackWave ships five distinct instruments, each covering a different quadrant.

**The simulator.** It drives many virtual nodes plus the in-memory store through compressed virtual
time, with seeded fault injection: reorderings, crashes, clock skew, lost hints, node isolation. One
64-bit seed fully determines a run. Any failure replays exactly from its seed. The method is borrowed
from TigerBeetle.

**The seed minimizer.** A failing run becomes a plan: a scenario plus a fault map addressed by stable
identity, never by draw order. The minimizer removes faults one at a time and re-checks that the same
invariant still trips. Because removal only ever calms a run, minimization is exact. What lands in the
repository is a small, checked-in regression, not a 300-step trace.

**The VOPR runner.** A forever-running search. It draws fresh seeds without end, and on a failure it
writes the plan and keeps going instead of halting. Bug-finding stops being a fixed battery on a pull
request and becomes a background process.

**The conformance suite.** The simulator stops at the storage contract, so it cannot catch a wrong
`SKIP LOCKED` query. The conformance suite is the sequential correctness test against the real
database. It also ships as a public package, `BackWave.Conformance`, so a custom adapter can prove
itself against the same contract.

**The torture suite.** The concurrent, non-deterministic correctness instrument. A randomized workload
hammers a real database, then invariants are audited over the final state, the transition log, and a
client-side journal of what each connection observed. A torture failure is always a bug.

Two more instruments sit alongside these. The benchmark harness measures performance only, and it is
deliberately outside the determinism boundary, so a noisy run is never a bug. The upgrade harness
boots an old schema, migrates it in place while jobs are in flight, and asserts that a mixed-version
fleet still works.

The point of the list is not the count. It is that each instrument states exactly what it proves, and
refuses the quadrant next to it. Deterministic correctness, sequential correctness against a real
database, concurrent correctness against a real database, and performance are four different questions
with four different tools.

## Testing your own jobs

TickerQ's testing story is the ordinary one. Run the scheduler, wait for real time to pass, and assert
the result.

BackWave ships `BackWave.Testing`, a harness on virtual time:

```csharp
var harness = new BackWaveHarness(BackWaveJobs.CreateRegistry(), services);

var jobId = await harness.EnqueueAsync(new SendReminder("order-42"), delay: TimeSpan.FromDays(2));
await harness.AdvanceAsync(TimeSpan.FromDays(3));

var job = await harness.Monitor.GetJobAsync(jobId);
```

No database, no container, no `Thread.Sleep`, no flake. A two-day delay is one line and completes in
microseconds. The harness also exposes `harness.BeginTransaction()`, so a rolled-back transaction
means the job never existed, which is how you test transactional enqueue.

For anyone who has written `await Task.Delay(5000)` in a job test and then watched it fail on a loaded
CI machine, this is the difference that shows up every week. The full story is in
[Testing your jobs](/docs/testing/testing-your-jobs).

## Observability

**TickerQ** ships an OpenTelemetry instrumentation package. It registers an activity source named
`"TickerQ"` and emits a job execution span with child spans for the lifecycle events: enqueued,
completed, failed, cancelled, skipped, and the two seeding events. Every event is also written through
`ILogger` with structured properties. Tags are TickerQ-owned names, for example `tickerq.job.id`,
`tickerq.job.function`, and `tickerq.job.machine`. There is no metrics pillar.

**BackWave** ships `BackWave.OpenTelemetry` with all three pillars, on the OpenTelemetry semantic
conventions rather than on private names:

- **Traces** use `messaging.system=backwave`, `messaging.destination.name` for the queue,
  `messaging.destination.template` for the Wire Name, `messaging.message.id` for the job id, and
  `messaging.consumer.group.name` for the worker group. The span shape is `send` to `receive` to
  `process`, with links for fan-in.
- **Metrics** use `messaging.client.sent.messages`, `messaging.client.consumed.messages`, and the
  `messaging.process.duration` histogram. BackWave adds its own instruments where the conventions have
  no equivalent, including `backwave.schedule.delay`, `backwave.job.queue.wait`,
  `backwave.jobs.dead_lettered`, `backwave.worker.slots.active`, and `backwave.queue.depth`.
- **Store spans.** Each adapter emits `db.*` spans and classifies store faults, so a slow claim query
  is visible as a database span and not as an unexplained gap.
- **Exemplars** link a slow histogram bucket straight through to the trace that produced it.

The practical result is that BackWave's telemetry lands in a generic messaging dashboard with no
custom queries. TickerQ's telemetry needs a TickerQ-shaped dashboard. See
[OpenTelemetry](/docs/dashboard-and-operations/opentelemetry) for the full surface.

BackWave also has two features with no TickerQ counterpart. The transition log is an append-only,
per-job history of state changes, governed by a job history policy with three levels. The transition
observer is host-supplied, egress-only code that BackWave invokes when a job reaches a declared state,
for example "Dead-Lettered, so post to Slack." It observes the core's outputs and can never alter a
decision. It is delivered at-least-once and it is deliberately not Effect-Once, because the reaction
is a new side effect outside the fence. That limit is documented rather than hidden.

## Schema, upgrades, and fleets

TickerQ persists through EF Core, so schema changes arrive as EF migrations. A setting moves the
tables to another schema, with a default of `"ticker"`. All TickerQ packages must be version-matched
with each other.

BackWave treats the schema as a contract with three named guarantees:

- **In-place upgrade.** The schema upgrades on a live database while jobs stay in flight. No drain, no
  maintenance window. This is the supported path for every adapter, not a best case.
- **Mixed-version fleet.** During a rolling deploy the cluster runs two adjacent versions at once.
  BackWave supports exactly N-1 skew, and a harness proves that a node one release behind still works
  against the upgraded schema.
- **Coordinated migration.** When a fleet cold-boots with auto-migrate on, every node attempts the
  migration and a database-level lock orders them. One applies it, the rest wake, re-check the
  version, and find nothing to do. No node is elected, so no node is special.

A schema-diff gate in CI fails the build when a migration is not additive. BackWave's schema name is
configurable per adapter, and the adapters author every query against the literal `backwave` and
rename it at one choke point, so the default emits byte-identical SQL.

## Dashboard and operator actions

TickerQ's dashboard is a Vue application. It is genuinely capable: you can create, edit, and delete
tickers from the UI, watch executions live, and inspect a chain. Authentication is configurable as
basic auth, an API key, host authentication, or a custom callback.

One point deserves a plain statement: **TickerQ's dashboard has no authentication by default**. The
documentation says so directly, and it offers `WithNoAuth()` as the explicit form. That is a
reasonable default for a local development loop, and a serious risk in a deployment where somebody
forgets to configure it.

BackWave takes the opposite position on both counts.

**Authorization is delegated and explicit.** There is a fixed set of dashboard permissions: View,
ViewSensitiveData, Requeue, Cancel, TriggerSchedule, PauseQueue. Each maps to a policy name or a
predicate in your own application. BackWave never owns users or roles. ViewSensitiveData is a separate
gate over raw content that can carry secrets: payload bytes, failure detail, and job output. A reader
can therefore see the dashboard without seeing the contents.

**Every write is a defined state transition.** An operator action is requeue, cancel, trigger a
schedule now, pause or resume a queue, cancel a workflow, or restart or retry a workflow. Each one is
a state-machine transition with recorded identity, and never a raw row edit. Editing a job's payload
is deliberately not an operator action, because a payload edit is an unauditable change to work that
is already in flight.

That is a real trade. TickerQ lets an operator fix a bad job from the UI. BackWave makes you
re-enqueue it from your own application, and gives you an audit trail in exchange. Which one you want
depends on who has dashboard access.

BackWave also has an MCP server that exposes the same reads and audited writes as Model Context
Protocol tools, under the same delegated authorization. Tools the caller is denied are hidden from the
tool list. It creates no new capability, only a new surface for an AI agent.

## Chaining and workflows

**TickerQ** chains with a fluent builder. A parent TimeTicker declares children, and a run condition
decides when a child runs: on success, on failure, on cancellation, on failure or cancellation, on any
completed status, or in parallel with the parent. The builder is capped at 5 children, and 5
grandchildren per child. Chaining applies to TimeTickers only, not to cron occurrences.

**BackWave** splits the same territory into two layers.

Below the determinism boundary sits the dependency: a static edge from a job to a parent set whose
terminal states gate the job's due-ness, implemented as a countdown latch. It has exactly two reaction
modes, on-success and on-any-terminal. It is simulated and fenced like any core state, with no fan-out
cap.

Above the boundary sits the workflow: the user-facing identity, name, graph view, and lifecycle
operations over a set of jobs connected by those edges. The typed builder references a step by its
.NET type rather than by a string:

```csharp
public sealed class CheckoutWorkflow : IWorkflow<CheckoutSeed>
{
    public void Build(TypedWorkflowBuilder builder, CheckoutSeed seed)
        => builder.Then(new ValidateOrder(seed.OrderRef))
                  .Then(new AuthorizeCharge(seed.OrderRef))
                  .Then(new PackShipment(seed.OrderRef), after: [typeof(AuthorizeCharge)]);
}
```

The `after:` argument names the gating parents by type, so fan-out and fan-in are compile-checked. A
step emits a typed result and an ancestor's result is read back by type. The output is written to the
job row atomically with the Succeeded transition, on the same fence, so a fenced-out
outcome discards it. Over-limit output is rejected rather than truncated, because a clipped serialized
blob is undeserializable and silent truncation is data corruption.

Two recovery paths exist. Workflow restart re-instantiates the definition as a brand-new workflow with
fresh identities and re-runs the whole graph. Workflow retry moves terminal members back to a
non-terminal state in place, with three scopes: all members, failed members only, or failed members
plus their downstream dependents.

Three shapes go past a plain dependency graph. A conditional gate declares both arms at build time and
evaluates at run time against an ancestor's decided output, then cancels the arm that was not taken.
Compensation attaches a saga-style undo to a step, so a failure downstream runs the declared reversal.
A child splice puts a whole second workflow in as a step. All three are declared in the same
compile-checked builder. See [Workflows](/docs/core-concepts/workflows).

**An honest limit, stated by BackWave itself.** This is not durable execution. There are no signals,
no durable waits, and no replay. The set of nodes is fixed when the workflow is built: a gate picks a
pre-declared arm, but nothing at run time adds, skips, or reorders nodes. If you need a step that
pauses for a human approval three days from now, you want a durable-execution engine such as Temporal,
not a job system.

TickerQ's chaining is not durable execution either. Its run conditions decide whether a successor runs
from the parent's terminal status. BackWave's advantage here is the typed builder, typed outputs,
gates, compensation, child workflows, the absence of a fan-out cap, and the fact that the gating logic
is simulated.

## Performance

**No head-to-head BackWave-versus-TickerQ benchmark exists.** This report does not claim one, and it
does not estimate one. BackWave's own measured numbers, its two-mode harness, and the rule that only a
pinned native instance produces a publishable result are documented under
[Performance and benchmarks](/docs/advanced/performance-and-benchmarks), and the measured comparison
against Hangfire is in [BackWave vs Hangfire](/compare/hangfire).

One architectural note without a number attached. TickerQ sleeps until the next due time rather than
polling on a fixed interval, which is a good fit for sparse scheduled work. BackWave now reaches the
same idle behavior with an opt-in knob, `WorkerGroupOptions.MaxPollInterval`. Left at its default the
worker group polls at the fixed `PollInterval`, exactly as before. Set `MaxPollInterval` above it and
an idle group backs off from `PollInterval` toward that ceiling, sleeping until the store's reported
next-due time, and it returns to `PollInterval` the instant a poll claims work or finds work due now.
A Wake-Up Hint triggers an immediate poll but does not by itself reset the interval.
Polling stays the correctness mechanism, so worst-case latency for newly enqueued work is at most
`MaxPollInterval` even if every hint is lost. On Postgres, through LISTEN/NOTIFY, and on SQLite,
through in-process hints, an enqueue still wakes an idle group in milliseconds, and the ceiling only
bounds the rare lost-hint case. On SQL Server, which has no hint channel, the ceiling is the direct
idle-latency bound. The feature shipped non-breaking, in a minor release, as a default method on the
storage contract. Under a dense queue backlog the knob is moot, because a group that keeps claiming
never idles and stays at `PollInterval`. How the two compare on that dense path is still unmeasured by
either project.

## Where TickerQ wins today

This section is not a courtesy. Each item is a real reason to pick TickerQ.

1. **The license.** TickerQ is MIT and Apache 2.0. BackWave is source-available under PolyForm Shield
   1.0.0, which permits reading, running, modifying, and forking for your own use, but forbids using
   BackWave to build a competing product. Some organizations have a policy that rules that out.
   BackWave does not call itself open source, because a no-compete license and the OSI definition are
   mutually exclusive.
2. **Community and proof of adoption.** 3.6k GitHub stars, an active Discord, and an OpenCollective.
   BackWave is newer and its contributions are invitation-only under a CLA.
3. **More persistence choices.** Redis, MySQL, and MongoDB have no BackWave equivalent. BackWave ships
   durable adapters for Postgres, SQL Server, and SQLite only.
4. **Cross-application scheduling.** TickerQ Hub and its remote executor let one application schedule
   work that another application runs. BackWave has no equivalent, because a BackWave node is a peer
   that coordinates only through the database.
5. **Dashboard job management.** Creating and editing a scheduled job from the UI is convenient, and
   BackWave refuses it on purpose.
6. **Per-instance retry configuration.** TickerQ sets `Retries` and `RetryIntervals` on the individual
   ticker, so two instances of the same work can retry differently. BackWave 1.6 closed most of this
   gap with `[Retry]`. The attribute overrides the worker-group policy for one job type at compile
   time, but the shape is fixed per type. There is still no per-enqueue override.
7. **A smaller surface to learn.** TickerQ has two entities and eight statuses. For a workload that is
   a handful of timers, BackWave is more machinery than the job requires.

## Which one to pick

**Pick TickerQ when** the license must be permissive, when you want to start in minutes with no
database, when you need Redis or MySQL or MongoDB persistence, when one application must schedule work
for another, or when your workload is a set of cron jobs and timers with modest correctness stakes.

**Pick BackWave when** a duplicated or lost job costs real money, when you run a cluster and need a
crash to heal itself without a Redis dependency, when you need a cluster-wide cap on a queue, when you
want your own job logic under test without a container, when you deploy often enough that in-place
upgrades and N-1 fleets matter, or when your telemetry has to land in a standard messaging dashboard.

## Caveats

1. **TickerQ moves fast.** Every TickerQ claim reflects the `main` branch and the published
   documentation as read on 2026-08-15. A later release can change any of them, and the crash-recovery
   gap above is exactly the kind of thing a maintainer fixes.
2. **No head-to-head benchmark was run.** Nothing here estimates one.
3. **Feature lists are not experience.** TickerQ has thousands of users running it in production.
   BackWave has a larger correctness apparatus and a shorter production history. Both facts are
   relevant.
4. **Workflows are a BackWave Pro feature.** Everything else described here is in the free packages.
   Pro is free for organizations under $1M annual revenue, enforcement is offline and soft-fail, and a
   missing license never disables a feature.

## Sources

**TickerQ**: [tickerq.net](https://tickerq.net) documentation, and the
[Arcenox-co/TickerQ](https://github.com/Arcenox-co/TickerQ) `main` branch. The files quoted are
`src/TickerQ.EntityFrameworkCore/Infrastructure/TickerQueryExtensions.cs`,
`src/TickerQ.EntityFrameworkCore/Infrastructure/TickerEFCorePersistenceProvider.cs`,
`src/TickerQ.Caching.StackExchangeRedis/NodeHeartBeatBackgroundService.cs`,
`src/TickerQ/Src/BackgroundServices/TickerQSchedulerBackgroundService.cs`, and
`src/TickerQ/Src/BackgroundServices/TickerQInitializerHostedService.cs`.

**BackWave**: the [documentation](/docs) on this site, and the
[source repository](https://github.com/Back-Wave/BackWave).
