Quartz.NETQuartz.NET
Home
Features
Blog
Discussions
NuGet
GitHub
Home
Features
Blog
Discussions
NuGet
GitHub
  • Getting Started

    • Overview
    • Quartz 4 Quick Start
    • Tutorial
      • Using Quartz
      • Jobs And Triggers
      • More About Jobs & JobDetails
      • Job Data
      • More About Triggers
      • Querying Jobs and Triggers
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Time and TimeProvider
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Execution Middleware
      • Job Stores
      • Configuration, Resource Usage and Building a Scheduler
      • Building a Scheduler Without a Host
      • Clustering
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Testing
      • Compile-Time Checks
      • Declaring Jobs with Attributes
    • Configuration Reference
    • JSON Configuration
    • Cron Expression Reference
    • Multi-Tenancy
    • Comparison
    • Frequently Asked Questions
    • Best Practices
    • Before You Go Live
    • Operating a Cluster
    • Log Events
    • Tenancy Patterns
    • Database Schema
    • Database Schema Changes
    • Migration Guide
    • Troubleshooting
    • API Documentation
  • How To's
    • One-Off Job
    • Rescheduling Jobs
    • Retrying Failed Jobs
    • Job Continuations
    • Multiple Triggers
    • Job Template
    • Running Quartz under Aspire
    • Quartz.NET with Wolverine
    • Coming from Hangfire
    • Coming from TickerQ
    • Embedding Quartz in a Library
    • Running under an External Leader Election
    • Publishing Trimmed and Native AOT
    • Extending Quartz: what is open, what is closed, and how to ask
    • A Job Store of Your Own
    • A Driver Delegate for a New Database
    • Persisting a Custom Trigger Type
    • A Lock Handler of Your Own
  • Packages

    • Quartz Core Additions

      • Jobs
      • Serialization (System.Text.Json)
      • JSON Serialization
      • Plugins
    • Integrations

      • Aspire Integration
      • ASP.NET Core Integration
      • HTTP API
      • HTTP Client
      • Dashboard
      • Hosted Services Integration
      • Microsoft DI Integration
      • Multiple Schedulers with Microsoft DI
      • Observability
      • Redis Lock Handler
      • TimeZoneConverter Integration
    • 3rd Party Plugins for Quartz
  • Quartz 3.x

    • Getting Started

      • Quartz 3 Quick Start
      • Tutorial
        • Using Quartz
        • Library Overview
        • Jobs And Triggers
        • More About Jobs
        • More About Triggers
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Tuning the Scheduler
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
      • Configuration Reference
      • JSON Configuration
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Tenancy Patterns
      • Troubleshooting
      • API Documentation
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Miscellaneous Features
    • How To's

      • One-Off Job
      • Multiple Triggers
      • Job Template
      • Using the CronTrigger
      • Rescheduling Jobs
    • Packages

      • Quartz Core Additions

        • Dashboard
        • Jobs
        • Serialization (System.Text.Json)
        • Serialization (Newtonsoft Json.NET)
        • Plugins
      • Integrations

        • ASP.NET Core Integration
        • Hosted Services Integration
        • Microsoft DI Integration
        • Multiple Schedulers with Microsoft DI
        • OpenTelemetry Integration
        • OpenTracing Integration
        • Redis Lock Handler
        • TimeZoneConverter Integration
      • 3rd Party Plugins for Quartz
  • Old Releases

    • Quartz 2.x
      • Quartz 2 Quick Start
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
        • CronTrigger Tutorial
      • Configuration Reference
      • Migration Guide
      • API Documentation
    • Quartz 1.x
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
      • API Documentation
  • License

Comparison

This page is an inventory, not a verdict. Five .NET libraries schedule background work, they disagree about almost everything below the word "cron", and the differences that matter are rarely the ones a feature list puts first. So every cell here was read out of the named project's own documentation or source at a pinned version, and every cell that is not Quartz's links what it was read from. Where Quartz.NET is the more expensive answer, the row says so, and Where Quartz.NET costs more than it is worth collects those admissions in one place.

Read as of 19 September 2026. A comparison rots; if a cell below no longer matches what the link says, the link is right and this page is wrong — please open an issue.

Two conventions. "None" means the project's own documentation has no such concept and a search of its source at the pinned version finds none — it is not a claim that the effect cannot be had by writing the code yourself, which it usually can. "—" means the row does not apply, because the library has nothing of that kind for the question to be asked of.

LibraryVersion readPinned at
Quartz.NET4.1this repository
Hangfire1.8.25tag v1.8.25
TickerQ10.4.0commit c6ed1e7d — there is no v10.4.0 tag, and this is the commit the 10.4.0 packages' SourceLink metadata names
Wolverine6.35.0tag V6.35.0
Coravel6.0.2commit 88ea3e89 — the repository has no tag past 4.0.3, and this is the commit the 6.0.2 nuspec names

What each one is

They are not five of the same thing, and half of the differences below follow from that.

Quartz.NET is a scheduler: a trigger is a stored object with a schedule, a time zone, a misfire instruction, a calendar and a priority, and the scheduler's job is to fire it once, on time, on one node of however many are running.

Hangfire is a background job processor first and a scheduler second. Its centre is a queue with a durable state machine — enqueued, processing, succeeded, failed — and a dashboard built around that state machine; recurring work is a cron string that enqueues into it.

TickerQ is a source-generated dispatcher. A method carries an attribute, a Roslyn generator writes the registration, and the dispatch path uses no reflection. Its two scheduling kinds are a one-shot ticker and a cron ticker.

Wolverine is a message bus that grew a cron in 6.34. opts.Schedules.ScheduleRecurring publishes a message on a schedule, over the delivery, durability and replay the bus already had. It is deliberately not a scheduler, and Quartz.NET with Wolverine is about which of the two should own a given schedule.

Coravel is an in-process convenience layer for ASP.NET Core: a scheduler, a queue, a cache and an event dispatcher, all in memory, all in one small package.

Declaring a job

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
The unit of worka class implementing IJob or IJob<TInput>an expression tree naming a method — no interface to implementa method carrying [TickerFunction("name")]a message type and its handlera class implementing IInvocable
Scheduling itAddJob<T> + AddTrigger<T>, or ScheduleJob<TJob, TInput>(input, delay)BackgroundJob.Enqueue, .Schedule, RecurringJob.AddOrUpdate(id, …, cron)by function name — new TimeTickerEntity { Function = "send-welcome" } — or by type after MapTicker<T>()opts.Schedules.ScheduleRecurring<T>("0 2 * * *")scheduler.Schedule<T>().EveryTenMinutes()
Registration in fullbuilder.AddQuartz() + builder.AddQuartzHostedService()services.AddHangfire(…) + services.AddHangfireServer()builder.Services.AddTickerQ(); + app.UseTickerQ();inside UseWolverineservices.AddScheduler() + app.Services.UseScheduler(…)
What the compiler checksthe job type and, for IJob<TInput>, its payload typethe method call, because it is an expression tree; the recurring id is a stringthe function name, its signature and its cron literal — TQ003 is a build error for a cron that will not parsethe message type; a bad cron throws at the registration line rather than at start-upthe invocable type; a cron string is parsed at run time
Where the schedule livesin the job store — added, rescheduled and deleted while the host is up, by any nodein the storage, changed at run timein the persistence provider, changed at run time or from the dashboardin code, at UseWolverine; the set is whatever the process was compiled within code, at UseScheduler; nothing is persisted

Through 4.1 Quartz read its cron at run time alone, so an expression that could not parse was an exception rather than a build error and TickerQ's generator won this outright. 4.2 closes it: an analyzer inside Quartz.nupkg reads a cron literal or const with the very parser that reads it at run time and fails the build on one that does not parse (Compile-Time Checks), and [QuartzJob] and [CronTrigger] declare a job and its schedule on the class for a source generator to register.

Trigger kinds and cron grammar

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Schedule kindsfive: cron, simple, calendar-interval, daily-time-interval and RFC 5545 recurrencefire-and-forget, delayed, recurring, continuation, and batches on the paid tiertwo: TimeTicker (one-shot) and CronTicker (recurring)a scheduled message, and a recurring one on cronfluent intervals, or a cron string
Cron parserQuartz's ownCronos 0.11.1, ILRepack-internalized into Hangfire.CoreNCrontab 3.3.0Cronoshand-written
Fieldssix, or seven with a trailing year, plus a five-field CronFormat.Unix modefive, or six with a leading seconds fieldsix with seconds; a five-field expression is expanded with 0 secondsfive, or six with a leading seconds fieldfive
Beyond * , - /L, W, #, H hashing, wrapping rangesL, W, #, and nothing beyond what Cronos parsesnone — NCrontab's grammar is * , - /, digits and namesthe same Cronos grammarnone
The finest cadence it will honourno floor; the seconds field means what it saysthe server's SchedulePollingInterval, 15 seconds by default — a six-field expression parses, but the documentation describes recurring jobs as minute-basedMinPollingInterval, one second by defaultfive seconds; quicker is refused where it is writtenone second for a fluent interval; a cron schedule is only evaluated on the minute mark
Breaking a tie between two due at oncePriority on the triggernone — an open request since 2022; on SQL Server queues are drained in alphanumeric orderTickerTaskPriority per function, deciding dispatch order rather than which occurrence winsan occurrence is a message, so the receiving endpoint decidesnone

A firing the process was down for

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
The policya misfire instruction per trigger: skip it, fire one catch-up, or fire every one that was missedMisfireHandlingMode per recurring job — Relaxed (the default: one job however many were missed), Strict (one per missed occurrence), Ignorable (none)none. An overdue row is picked up by the fallback sweep and run late; SkipStaleCronOccurrencesOnStartup() opts into dropping stale occurrences and is off unless calledno back-fill; the one occurrence already pre-scheduled still fires, and the rest of the window is lostnothing. The tick catch-up the documentation describes is seeded at process start, so it covers a stalled timer and not a restart
Where the choice is madeon the trigger, so two schedules in one application can differper recurring job———

Calendars and time zones

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Dates it must not fire onsix calendars on the trigger — HolidayCalendar, CronCalendar, DailyCalendar, WeeklyCalendar, AnnualCalendar, MonthlyCalendarnonenonenonenone
Time zoneper trigger, with InTimeZoneRecurringJobOptions.TimeZone, UTC unless setone SchedulerTimeZone for the whole scheduler, the machine's local zone unless setper schedule, UTC unless supplied.Zoned(TimeZoneInfo) per schedule, UTC by default

For everyone except Quartz, "not on public holidays" is a condition the job body checks. A calendar is the one axis on this page where nobody else competes.

Running once when several nodes are up

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
How one node winstrigger acquisition takes a row lock before the job runsa distributed lock around the recurring enqueue; the enqueued job is then taken off the queue by whichever server gets there firsta conditional UPDATE whose affected-row count decides — no row lock, no SKIP LOCKED, no lock table; the Redis provider uses Lua scripts insteadone SingularAgent per cluster, reassigned on failover, plus a deterministic deduplication idnothing — the scheduler is a process-local Timer, so every instance runs every schedule
A node that dies mid-executioncheck-in detects it, and a job that requests recovery is re-runa server that stops heartbeating is removed after ServerTimeout (5 minutes) and its jobs are requeuedthe fallback sweep picks up rows whose lease has gone stale; the Redis provider has a dead-node recovery scriptthe agent moves to another node—
Pinning work to one nodePreferredNode on the triggerqueue names, with each server subscribing to a different setNodeIdentifier identifies the lease holder; it does not route workthe agent's own assignment—

The guarantee is worth stating exactly rather than in marketing terms. Quartz fires each trigger once, because acquisition is the lock and it happens before the job runs; a firing is then at most once unless recovery is asked for, which buys at least once instead. Nobody here promises exactly once, and Hangfire's own documentation says as much in two places — its best practices page asks for re-entrant methods because an interruption "can be caused by many different things (i.e. exceptions, server shut-down), and Hangfire will attempt to retry processing many times", and its throttling page warns that a mutex "doesn't prevent simultaneous execution of the same background job". Write the job so a second run is uneventful whichever library you pick — Best Practices has the shapes.

Concurrency control

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Overall parallelismMaxConcurrency on the thread pool, ten by defaultWorkerCount, min(ProcessorCount × 5, 20)MaxConcurrency, Environment.ProcessorCount by defaultthe receiving endpoint's own concurrencynone documented
One job not overlapping itself[DisallowConcurrentExecution] — cluster-wide with a persistent store, and enforced by the store rather than by a waitDisableConcurrentExecution(timeoutSeconds), a distributed lock that waits and then throws DistributedLockTimeoutExceptionmaxConcurrency on [TickerFunction], a SemaphoreSlim in this processan occurrence is a message like any otherPreventOverlapping, an in-memory mutex
Capping a whole category of workexecution groups, with each limit counted per node or across every node sharing the storeHangfire.Throttling on the Business tier — mutexes, semaphores and rate limiters, documented as "best-effort" and as not suitable "for workloads where several hundreds of background jobs compete for the same semaphore"per function and per process only; nothing counts across nodes——

Quartz's cluster-scoped execution limit is the one thing in this table that nothing else has for free: "this tenant gets eight threads, however many nodes are up" is a limit the store counts, not a number each process keeps its own copy of.

Retries and failure

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Retry on failureRetryPolicy on the trigger — Fixed, Exponential or Explicit, opt-in per triggerAutomaticRetryAttribute, applied to every job by default: ten attempts, delay (attempt − 1)⁴ + 15 + rand(30) × attempt secondsRetries and RetryIntervals in seconds, per tickerthe message's own retry policies and dead-letter queuenone — OnError and a ScheduledEventFailed broadcast, with no re-invocation
What holds the waitthe job store: a retry is a new fire time on the trigger, so it survives a restart and any node can run itthe storage: the job sits in the Scheduled state until its next attemptan in-process Task.Delay inside the execution, so the worker slot and the row's lease are held for the whole sequence and it dies with the processthe message store—
Jitteropt-in on Exponential: each wait is drawn from [1 − jitter, 1 + jitter] times what the backoff saysyes, rand(30) × attempt secondsnonethe bus's own policy—
When the attempts run outthe trigger returns to its ordinary schedule; it is not parked in an error state. ITriggerListener.TriggerRetriesExhausted, a log event, a counter and a history row marked final all say it happenedthe job lands in Failed, which never expires and can be requeued from the dashboardthe ticker is recorded as faileddead-letter queue—
Per-job timeout[JobTimeout] on the job class, enforced once AddJobTimeout registers the middlewarenonenonethe message's ownnone

Hangfire's retry is on by default and Quartz's is opt-in, which is a real difference in what a careless application gets. Hangfire jitters unconditionally where Quartz's jitter is a number on the policy, so a schedule that wants its waits exact keeps them. What Quartz has that the others do not is that the wait is held in the store rather than in the process: a node that dies during a five-minute backoff does not take the retry with it.

Continuations and chaining

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Run B after AJobChainingJobListenerBackgroundJob.ContinueJobWith(parentId, …)parent/child TimeTickersa handler that publishes the next messagenone
Conditional on the outcomeno — the listener fires the follow-up on any completion, including one that threwJobContinuationOptions: OnAnyFinishedState, OnlyOnSucceededState (the default), OnlyOnDeletedStateRunCondition: OnSuccess, OnFailure, OnCancelled, OnFailureOrCancelled, OnAnyCompletedStatus, InProgresswhatever the handler decides—
Where the link is keptin memory with the listener, re-registered on every start; the follow-up is fired rather than scheduled, so there is no trigger to seein the storage, with an Awaiting state in the dashboardin the persistence provider, on the child rowin code—
On a recurring scheduleyes — the listener is about job keysrecurring jobs enqueue ordinary jobs, which can be continuedno — chaining is TimeTicker onlyyes—

The table is the 4.1 state, and this was Quartz's weakest row in it: JobChainingJobListener calls itself "a poor man's workflow" in its own documentation, and it is — the links are not persisted, the follow-up runs on whichever node ran the parent, and a parent that threw still triggers it. 4.2 answers it. A trigger carrying StartAfter(parentTriggerKey, condition) waits in the job store, is settled by the parent's completion inside the parent's own transaction, and is released or discarded by the outcome it named; the listener remains as the recurring form and takes the same conditions. See Job Continuations.

Persistence

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Defaultin-memory RAMJobStorenone — a storage must be configuredin-memorynone for the schedulein-memory, and there is no other option
Databases the project itself shipsSQL Server, PostgreSQL, MySQL, Oracle, SQLite and Firebird, through ADO.NETSQL Server, plus Hangfire.InMemory; Redis is Hangfire.Pro.Redis, on the paid tierEF Core — the documentation names SQL Server, PostgreSQL and SQLite while the readme adds MySQL — plus RedisPostgreSQL, SQL Server, MySQL and SQLite, for the message store—
Anything elsea store of your own, or a dialect of your owncommunity packages for PostgreSQL, MongoDB, MySQL and SQLite, under an explicit "not responsible … does not provide official support" disclaimera provider of your own——
Creating the schemaProvisionSchema(), or run the DDL yourselfinstalled automatically from the storage's constructor unless PrepareSchemaIfNecessary says otherwisean EF Core migration of your ownWolverine provisions its own tables—

The dashboard, and what it allows before you configure anything

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
In the boxQuartz.Dashboard, free, thirteen pagesin Hangfire.Core, freeTickerQ.Dashboard, freenone; CritterWatch is the paid consolenone; Coravel Pro is a separate product
Who may reach it by defaultnobody, and the application will not start until you say — a mapping that carries neither RequireAuthorization nor AllowAnonymous, under a host with no fallback policy, fails at start-uplocal requests only; anything else means implementing IDashboardAuthorizationFiltereveryone. AuthMode.None is the default, and the documentation says so: "By default the dashboard has no authentication — it's publicly accessible."——
Read-only modeyes, and a job-type allow-list for what may be scheduled through itIsReadOnlyFunc, off by defaultno——
What it is best atthe whole cluster: node check-ins, execution groups and their headroom, misfires, an action log of what was done from itthe state machine — one page per state, and one-click requeue or delete of a failed job, singly or in bulklive SignalR monitoring, editing both ticker kinds, starting and stopping the host——

Hangfire's per-state lists are still better at browsing failures by kind. The "something failed last night, show me and run it again" workflow itself is answered: the History page has a Failed after retries filter and a Run again button on every occurrence that gave up — see When the policy gives up.

Observability

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Tracestwo job spans and thirty-three store spans on the Quartz activity sourcenone in the box; the OpenTelemetry community's instrumentation package is still pre-releaseTickerQ.Instrumentation.OpenTelemetry — tickerq.job.execute.* spans on a TickerQ sourcethe bus's own message spans, tagged wolverine.schedule.namenone
Metricseleven instruments on the Quartz meternonenone — the package emits traces and ILogger events onlyyes, on Wolverine's own meternone
Health checkin the core packagenonenoneWolverineFx.HealthChecks, a separate packagenone
.NET AspireQuartz.Aspire — a connection name becomes a persistent store with its telemetry and health checknonenone—none

Trimming and native AOT

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
DeclaredIsAotCompatible, with no IL3050 anywhere in the packagenoIsAotCompatible on four of the six shipped libraries — not the EF Core provider and not the OpenTelemetry packageIsAotCompatibleno
Checkeda canary application is published as a native executable and run, on Windows, Linux and macOS, on every pull requestthe maintainer's answer is "not supported yet"an AOT sample publishes nativelythe trim and AOT analyzers, on both target frameworks—
What you must still doname job types in a way the trimmer can see, or root them — the page says which paths still warn—supply a JsonSerializerContext through WithJsonContext for payloads——

Target frameworks

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
Targetsnet10.0; the 3.x line covers .NET Standard 2.0 and .NET Framework and is maintainednet451, net46, netstandard1.3, netstandard2.0net10.0; parallel 8.x and 9.x lines target net8.0 and net9.0net9.0 and net10.0net6.0
Serializing what is storedSystem.Text.Json, with Newtonsoft.Json available as a second serializerNewtonsoft.Json onlySystem.Text.Json, needing a JsonSerializerContext when trimmed——

Hangfire's framework list is the widest here by a distance, and that is a real reason to choose it: it is the only one of the five whose current line a .NET Framework application can take. Quartz's answer there is the 3.x line, which is maintained but is not 4.x.

Licence and price

Quartz.NET 4.1Hangfire 1.8.25TickerQ 10.4.0Wolverine 6.35Coravel 6.0.2
LicenceApache-2.0LGPL v3, or a commercial licenceMIT OR Apache-2.0, with a CLA for contributorsMITMIT
Free tiereverythingOpen: SQL Server and in-memory storage, community supporteverythingeverything in Wolverinethe library
Paid tiersnoneStartup $500, Business $1,500, Enterprise $4,500 per organization per year; batches and Redis storage are Pro, throttling is Acenone today. A commercial model was put to the community on 3 August 2026, which says that nothing changes today and that existing MIT and Apache releases remain free permanentlynone for Wolverine; CritterWatch and AI Skills are the family's paid productsCoravel Pro: free for personal use, $299 a year commercial

Where Quartz.NET costs more than it is worth

Collected in one place, because a comparison that only flatters its author is not worth reading.

The first five minutes are longer. TickerQ is two registration lines and an attribute on a method; Coravel is two lines and a fluent chain. Quartz's shortest form is comparable, but the moment a recurring schedule and a database appear it is a builder chain, a connection string, a driver package of your own and a schema to create. That is the price of a schedule that outlives the process, and it is a price — not everybody needs to pay it.

A schedule costs a row. With a persistent store, adding a schedule is an INSERT and a round trip, and every firing is a handful more. TickerQ's in-memory default and Coravel's whole model are a dictionary insert. For thousands of short-lived one-off firings, that difference is real, and the answer is one durable job per job type with a trigger per firing rather than a pretence that the row is free.

A bad cron was a run-time exception rather than a build error, through 4.1. TickerQ's source generator won this outright, and 4.2 is where Quartz answers it: a cron literal or const is read at build time (Compile-Time Checks), and a job can declare its schedule on its class (Declaring Jobs with Attributes). An expression assembled at run time is still read at run time, and for that CronExpressionBuilder and the "when does this fire" helper are what Quartz offers.

Continuations were a listener, not a contract, through 4.1. 4.2 makes them a trigger the store holds: see Continuations and chaining above and Job Continuations.

Retry is opt-in, and so is its jitter. Hangfire retries every job by default and spreads the attempts. Quartz retries only the triggers you gave a policy to, and the waits are exactly what the policy says — unless the policy names a jitter, which spreads them the same way.

There are no queues. Hangfire's [Queue] with several servers each subscribing to a different set is a routing mechanism, and Quartz has nothing that routes. Execution groups bound how much of a category runs at once; they do not decide which node takes it, and PreferredNode pins rather than balances.

If the occurrence is a message, a bus may be the better home. An application that already runs Wolverine, already has an outbox and wants "publish X every weekday at 03:00" should use Wolverine's own schedule rather than a second runtime and a set of tables. Quartz.NET with Wolverine is that argument in full, including the cases where it goes the other way.

.NET Framework is 3.x territory. Quartz 4.x is net10.0 only. Hangfire still ships net451.

Coming from one of these

  • Coming from Hangfire — the API mapping, and the semantics that differ
  • Coming from TickerQ — the same, shorter
  • Quartz.NET with Wolverine — running both in one host, and deciding which owns a schedule
Help us by improving this page!
Last Updated: 9/19/26, 9:30 PM
Contributors: Marko Lahma, Claude Opus 5 (1M context)
Prev
Multi-Tenancy
Next
Frequently Asked Questions