| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| Quartz.NET-4.4.0.zip | 2026-10-07 | 48.3 MB | |
| README.md | 2026-10-06 | 24.8 kB | |
| v4.4.0 source code.tar.gz | 2026-10-06 | 8.8 MB | |
| v4.4.0 source code.zip | 2026-10-06 | 11.0 MB | |
| Totals: 4 Items | 68.1 MB | 1 | |
Quartz.NET 4.4 is history you can run a business on. Each run says what it achieved: succeeded, failed, cancelled or skipped, with a summary and metrics. The history keeps that per job, sweeps it by result and charts it over time, and a health check degrades when a required job stops succeeding. Recovery respects rolling deploys, and triggers already due fire in the transaction that acquires them: at 20 triggers a second on PostgreSQL, 95–99 % of firings land within ±50 ms, up from 54 % on 4.3.0. Quartz.Weasel now manages the job store's schema on six databases, adding MySQL, Oracle and Firebird.
:::shell
dotnet add package Quartz --version 4.4.0
Highlights
- A run says what it achieved, and the history keeps it per job.
context.Result, a status per job, retention by result, and a chart of runs over time. Execution history and outcomes - A health check per job.
RequireSuccessWithin(jobKey, TimeSpan.FromHours(26))degrades when a nightly job stops succeeding. Execution history and outcomes - A retry policy on the job class, and a scheduler-wide default. Also
RunAtStartup, scheduling a trigger already paused, and pausing a set of keys with a reason in one call. Scheduling - Fire on acquire. One transaction per round instead of two: one-off throughput on PostgreSQL goes from 258–272 to 420–429 firings a second. Reliability and clustering
- Recovery that respects rolling deploys. A firing cancelled by a graceful shutdown can be replayed by a peer or the restarted node. Reliability and clustering
- Weasel for six databases.
Quartz.Weasel.MySQL,Quartz.Weasel.OracleandQuartz.Weasel.Firebirdjoin PostgreSQL, SQL Server and SQLite. Databases and Weasel - An HTTP client that reads a newer host, and HTTP API errors as problem details under native AOT. HTTP API and dashboard
Execution history and outcomes
- Job outcomes (#3971, closes [#3957]). A job says what its run achieved:
context.Result = JobRunReport.Skipped("…").With("released", 3). - Results:
JobRunResult {Succeeded, Failed, Cancelled, Skipped}. - A summary and metrics go with it. Metrics are JSON, AOT-safe, capped at 4,000 characters (log events 1059/1060).
- Vetoes are recorded in the misfire feed (
MisfireReason.Vetoed). Manual runs (TriggerJob) are marked. - Per-job
JobRunStatus: last run, last success and failure, consecutive failures, counts. - Retention by result (
RetentionByResult,MisfireRetention) andMaxEntriesPerJob. - How-to: Job Outcomes.
- The database history keeps all of it (#3980, closes [#3959]; schema [#3972], closes [#3958]).
- Each run's row and its job's status commit in one transaction on the history connection.
- The sweep is tiered by result, capped per job, and clears status rows of deleted jobs. One elected node sweeps per cluster.
- The misfire feed is filtered by reason. A 4.2 row with no reason reads as Missed.
- Known cost: runs of one job that complete at once take turns on its status row. On PostgreSQL, one hot job with history on went from 113–248 to 77–189 firings a second. Tracked in #3979.
- Fixed: the optional-table refusal named
database/migrations/database/migrations/…. (#3972) - A run whose job threw records the job's own message (#3995, closes [#3992]). The history, the per-job status
and the HTTP API's
JobExecutedevent carry it, not the wrapper's "Job threw an unhandled exception". Log templates are unchanged. - Health check per job (#3982, closes [#3961]).
o.RequireSuccessWithin(new JobKey("nightly-report"), TimeSpan.FromHours(26)), or aRequiredJobslist bound from configuration.- It reports Degraded by default, per job. A Skipped run counts as a success.
- One status read per check.
- The host fails at startup if the execution history is off.
- Run statistics over time (#4027, closes [#4015]).
- New
IExecutionHistoryStore.QueryExecutionStatistics(a default member), with an HTTP route and a client member. - It returns time buckets of runs by result, with p50/p95/max duration. Percentiles are exact on every store:
native
PERCENTILE_CONTon PostgreSQL and Oracle, a ranked read elsewhere. - The dashboard shows a chart on History and on Job Detail and adds a Window filter (1h/24h/7d/30d). The stat cards count the window rather than the page.
- With the database history, the chart needs SQL Server 2012+, MySQL 8.0+, SQLite 3.25+ or Firebird 3+.
- Run a failure again with the input it had (#4021, closes [#4012]).
- Opt-in:
ExecutionHistoryOptions.RecordInputrecords a run'sJobInput, up toMaxInputBytes(default 16 KB). Over the cap the input is not kept, and the row is flagged. - The dashboard's Run again, on History and on the Execution page, passes the input back to
TriggerJob(key, data)and says whether it did. - Two nullable columns,
JOB_INPUTandJOB_INPUT_TOO_LARGE, in the optional 4.4 migration. - The History table fits at 1280 px: Run again is always on screen, and keys show the name over the group.
- Telemetry carries the run's result.
quartz.job.execution.durationgains aquartz.job.resulttag:succeeded,failed,cancelledorskipped, classified by the same rule as the history row.error.typeis unchanged: it is set on a failed run that threw. (#4004, closes #3967)- The
Quartz.Job.Executespan getsquartz.job.resulttoo. A failed store span now carrieserror.type. (#4020, closes #4016) - New
QuartzSchedulerOptions.RecordExceptionSpanEvents, since OpenTelemetry deprecated span events in 2026. Unset, Quartz followsOTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN, sologsturns the events off. The default is unchanged. (#4020)
Scheduling
- A retry policy on the job class, and a scheduler-wide default (#4028, closes [#4013]).
[RetryPolicy(3, "00:00:30")](Fixed),[RetryPolicy(5, "00:00:10", 2.0)](Exponential), Explicit delays, or[RetryPolicy(0)](never), plusq.UseDefaultRetryPolicy(…).- Precedence: the trigger's own policy, then the job's attribute, then the scheduler default.
RetryPolicy.Nonerefuses at any level. - It is resolved at fire time, so triggers already stored are covered, and nothing is written back.
- Analyzer QZ0006 checks the attribute's durations.
- An inherited policy with no room for a retry before the next occurrence is not reported as exhausted (debug event 1062).
SendMailJobandNativeJobcarry[RetryPolicy(0)].- Run a job once when the scheduler starts (#4024, closes [#4019]):
q.RunAtStartup(jobKey). - It runs on the first start, again after a restart, and when a scheduler built in standby is started.
- In a cluster it runs once on each node that starts.
- A run that a crash prevented is replaced by the next start's run, not added to it. Registering the same job twice gives one run.
@rebootis still rejected, and its message namesRunAtStartup.- Schedule a trigger already paused, with a reason (#4023, closes [#4018]).
ScheduleJobOptions/AddTriggerOptionsgainPaused,PauseReasonandPauseRequestedBy. The shipped stores write the trigger paused in the same write, so it never fires first.- A custom store answers through the new
IJobStore.SupportsStoringPauseddefault member. Without it, the scheduler pauses the trigger straight after storing it; the gap between the two is documented. - The HTTP schedule routes take the same fields. A host older than 4.4 is refused.
- Fixed: replacing a trigger no longer leaves an earlier pause's reason on the row.
- Pause a set of triggers or jobs with a reason in one call (#3986, closes [#3964]).
- New:
PauseTriggersWith/PauseJobsWithonIScheduler,IJobStoreandIQuartzApiClient. - One lock and one transaction in the database store.
- The existing
/keys/pauseroutes take the reason. A reasonless 4.3 client still gets the exact reasonless pause. - The dashboard's bulk pause is one call, and the Action Log records the reason on every pause.
- Job listeners hear progress (#4003, closes [#4001]).
- New
IJobListener.JobProgressChanged(context, progress), a default member. - It is raised at the cadence the progress is written: the first report at once, then at most once a second per firing, and only on a change.
- The last report is always heard before
JobWasExecuted. It is called off the job's thread and is in-process only. - A listener that throws is logged (event 1061), and the job and the other listeners carry on.
- Thanks to @bdovaz, who asked for it in discussion [#3996] and filed [#4001].
Reliability and clustering
- Fire on acquire (#4011, closes [#3864]). Triggers already due are fired in the transaction that acquires them.
- The database store makes one transaction per round instead of two, and sends each round's claims and fire writes as one batch each.
- The in-memory store takes one lock per round instead of two.
-
PostgreSQL, against the released 4.3.0 (three alternating sittings; the full table is under Measured):
Workload 4.3.0 4.4.0 20 triggers a second, firings within ±50 ms 54 % 95.5–99.2 % 100 triggers a second, firings within ±250 ms 68–70 % 99.5–100 % One-off throughput 258–272 a second 420–429 a second Two-node drain 293–298 a second 456–491 a second -
A custom
IJobStorekeeps the old two-step path through the newAcquireNextTriggersAndFireDuedefault member. - A custom
StdAdoDelegatekeeps every override it has. The newIDriverDelegate.SupportsFireOnAcquireanswers true only for the shipped delegates, and a subclass gets the one-transaction round through the per-trigger members it may override. - A clustered node looks again soon for a trigger that a serial job on another node holds back (#4032, closes [#3988]).
- Before, the node idled a full idle wait (15–30 s), and its pinned serial firings could drop to zero.
- Now it looks again after 100 ms, backing off to at most 5 s while the trigger stays held. On PostgreSQL the worst firing went from 12.9 s late to 0.37 s.
- Only clustered stores with the shipped delegates do this. Non-clustered stores and custom delegates are unchanged.
- The mixed-version gate no longer needs its heartbeat trigger.
- A graceful shutdown can hand a cancelled firing back for recovery (#4030, closes [#4014]). Opt-in:
AdoJobStoreOptions.RecoverFiringsCancelledByShutdown(keyquartz.jobStore.recoverFiringsCancelledByShutdown). - A
RequestsRecoveryjob cancelled by a graceful shutdown gets a recovery trigger instead of completing as Cancelled. A peer, or the restarted node, runs it on its next acquisition. - Continuations waiting on it follow the replay. The cancelled run's partial job data is not saved.
- A user
Interruptor[JobTimeout]stays a cancellation. The in-memory store is not affected. - Recovery triggers now keep the original firing's markers when recovered again.
- A trigger that keeps failing to fire is parked (#3973, closes [#3963]). After five non-transient failures
in a row it is stored ERROR. Before, it was released and acquired again forever, which hot-looped and starved
its
[DisallowConcurrentExecution]job-mates. AdoJobStoreOptions.MaxConsecutiveFireFailures(default 5, 0 = never), flat keyquartz.jobStore.maxConsecutiveFireFailures.- Log event 3050. Recover with
ResetTriggerFromErrorState. - A calendar that throws fails its own trigger, not the in-memory batch (#3981, closes [#3974]).
- The in-memory store parks such a trigger after
InMemoryJobStoreOptions.MaxConsecutiveFireFailuresfailures in a row (log events 2008/2009). - The scheduler releases the triggers it acquired but did not fire when a store fails the batch unexpectedly.
- A calendar that throws while a misfire is handled fails only its own trigger, in both stores (#4025, closes [#3985] and [#4006]).
- In-memory store: the acquisition pass, completion, overlap release and resume carry on.
- Database store: a completion no longer retries until shutdown with its job stuck
BLOCKED. - Failures are counted at most once per misfire threshold, so a briefly failing calendar heals instead of being parked within milliseconds. They are counted only after the store's own commit, and a successful misfire resets the count.
- The resume calls log a calendar's exception instead of throwing it.
- Fixed: a notification that threw after a commit made the store retry work it had already committed (log event 3053). New in-memory event 2010.
- An in-memory calendar replacement that throws changes nothing (#4029, closes [#4026]).
AddCalendar(…, updateTriggers: true)works out every trigger's new fire time on a clone first, and fails as the database store does, withJobPersistenceException.
Databases and Weasel
- Quartz.Weasel.MySQL and Quartz.Weasel.Oracle (#3987, closes [#3954] and [#3955]):
UseWeaselForMySql()andUseWeaselForOracle(). - MySQL: names are qualified by the connection's database. The migration takes a
GET_LOCKuser lock, which needs no privilege. - Oracle: names are qualified by
CURRENT_SCHEMA. There is no lock, becauseDBMS_LOCKneeds a grant. A conflicting apply re-reads and retries, asProvisionSchema()does. - Both are proven against real databases. Our fresh-install script and a Weasel-created schema read back unchanged; 3.20, 4.2 and 4.3 databases upgrade add-only; an application's own objects survive.
- Quartz.Weasel.Firebird (#4005, closes [#3990]):
UseWeaselForFirebird(), for Firebird 3, 4 and 5. - Built on Weasel.Firebird, which we contributed upstream as JasperFx/weasel#666. It first shipped in Weasel 9.39.0.
- There is no global lock: a racing apply re-reads the schema.
- Identifiers are limited to 31 characters by default.
MaxIdentifierLengthallows up to 63 on Firebird 4 and later. - Pull requests run the catalog tests on the official Firebird 3, 4 and 5 images.
- Weasel 9.39.0 (#4007, closes [#4002]). Every Weasel package Quartz references is
[9.39.0, 10.0.0). - Weasel's Oracle migrator lost the LONG fetch size and rebuilt
IDX_QRTZ_T_NFT_STon every apply. JasperFx/weasel#660 fixed it upstream, so Quartz.Weasel.Oracle needs no workaround. - A lock-free apply (SQLite, Oracle, Firebird) retries only a lost race, recognised by the provider's error
code. Every failure surfaces as a
SchedulerExceptionwith the original inside. - Quartz.Weasel.SQLite rebuilds a table carrying your own columns, indexes and foreign keys, keeping them and their rows. 4.3 refused. Thanks to the 11 upstream fixes in Weasel 9.36.0. (#3978, closes [#3956])
- Schema guard rails (#3989, closes [#3962] and [#3949]).
- All eight fresh-install scripts, SQL Server MOT and pre-2016 included, are held to the schema model: columns, types, keys, foreign keys and teardown.
- Migration chains and column lists are read from the migration folders.
- The mixed-version gate runs a released 4.3.0 node beside 4.4, with history on.
- SQL Server's BLOB foreign key:
ProvisionSchema()leaves it out too, as the fresh-install script always has. - The MySQL script loses its deprecated integer display widths.
Thanks to the JasperFx maintainers for reviewing and merging Weasel.Firebird (JasperFx/weasel#666). Weasel schema management
Database schema
| Script | Required | Adds |
|---|---|---|
4.4/add_execution_outcome_<dialect>.sql |
only with execution history on | RESULT, SUMMARY, METRICS, MANUAL, FIRE_INSTANCE_ID, JOB_INPUT, JOB_INPUT_TOO_LARGE on QRTZ_EXECUTION_HISTORY; IDX_QRTZ_EH_JOB_TIME; table QRTZ_JOB_STATUS (#3972, closes [#3958]; [#4021]) |
The new columns are nullable, and the script is safe to run beside 4.3 nodes. The Weasel models carry it.
HTTP API and dashboard
- History and status over the wire (#3977, closes [#3960]).
- History filters by job, fire-time window and result.
- Status routes under
/history/job-status, with a version check so a client never sends a 4.4 filter to an older host. - The misfire listing leaves Vetoed out unless
reasonsnames it, so a 4.3 reader never meets a name it can't parse. - Dashboard: a Result column (Succeeded/Skipped/Failed/Cancelled), summary and metric chips, a manual badge, Last run / Last success / failing ×N on Jobs, and a status panel on Job Detail.
- The HTTP client tolerates a newer host (#4022, closes [#4017]).
- An unknown event kind or a frame it can't read is skipped, and the live-event stream stays open.
- An enum name it doesn't know reads as a fallback or
null, or leaves only that item out of a listing. Each enum's rule is documented. Log events 9200–9203. - Unknown JSON members are always skipped. The host still refuses an unknown name in a request.
- From 4.4, the server no longer has to hide new names from clients.
- HTTP API errors are problem details in a trimmed or native AOT host (#3975, closes [#3965]). Every error used to be an empty 500. A second native canary runs the wire path on every pull request.
- Dashboard fixes (#3986): the paused note stays in the Details column; the Jobs table holds at 1280 px; the
AspNetCore example's dashboard works from source on .NET 10 (
MapStaticAssets), and ExamplesSmoke assertsblazor.web.js. - The run statistics chart and Run again with input are under Execution history and outcomes.
Behaviour changes
| Now | In 4.3 | PR |
|---|---|---|
A cancelled run is recorded as Cancelled (Succeeded false) |
recorded as a success | [#3971] |
context.Outcome reads Vetoed inside JobExecutionVetoed |
read Succeeded |
[#3971] |
Vetoed firings appear in the misfire feed (MisfireReason.Vetoed) |
not recorded | [#3971] |
TriggerJob firings carry QRTZ_MANUAL_TRIGGER in the trigger's JobDataMap |
no marker | [#3971] |
History, job status and the JobExecuted event carry the job's own exception message |
"Job threw an unhandled exception"; log templates unchanged | [#3995] |
A trigger that fails to fire 5 times in a row is stored ERROR, in both stores (MaxConsecutiveFireFailures, 0 = off) |
persistent: released and acquired again; in-memory: batch stranded | [#3973], [#3981] |
| A calendar's misfire failures count at most once per misfire threshold, after the store's commit | not counted | [#4025] |
| The resume calls log a calendar's exception | thrown | [#4025] |
WithRetryPolicy(null) means "inherit"; manual and recovery runs inherit too |
cleared the policy | [#4028] |
SendMailJob and NativeJob carry [RetryPolicy(0)]: the scheduler default never retries them; a trigger's own policy still applies |
no attribute | [#4028] |
A Weasel SQLite apply failure is a SchedulerException with the original inside |
Weasel's InvalidOperationException |
[#4007] |
| Quartz.Weasel.SQLite rebuilds a table carrying your own objects | refused | [#3978] |
The HTTP misfire listing leaves Vetoed out unless reasons names it |
— | [#3977] |
| Dashboard: Complete is Succeeded; Job Detail's history link opens that one job | Complete | [#3977] |
| Dashboard stat cards count the window | counted the page | [#4027] |
A firing committed on acquisition runs even if Shutdown() was called mid-round |
— | [#4011] |
A trigger fired on acquisition has no TriggersFired span; the acquisition span carries the count |
a TriggersFired span |
[#4011] |
OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN=logs turns exception span events off (RecordExceptionSpanEvents overrides) |
ignored | [#4020] |
| A clustered node re-polls after 100 ms, up to 5 s, for a trigger a serial job on another node holds | waited a full idle wait | [#4032] |
| Replacing a trigger clears an earlier pause's reason | reason left on the row | [#4023] |
| A recovery trigger recovered again keeps the original firing's markers | overwritten | [#4030] |
Mixed 4.3 and 4.4 nodes
- The 4.4 script is safe to run beside 4.3 nodes. A 4.3 node never touches
QRTZ_JOB_STATUS, so the per-job status counts only runs 4.4 nodes record until every node is 4.4. - A 4.3 reader never sees
Vetoedin the HTTP misfire listing. A 4.2 misfire row with no reason reads as Missed. - A 4.4 client never sends a 4.4 history filter to an older host, and scheduling a trigger paused on a host older than 4.4 is refused.
- A reasonless 4.3 client's key-set pause on a 4.4 host is the exact reasonless pause.
- CI runs a released 4.3.0 node beside 4.4 on one database, with history on. (#3989)
Upgrading
An application on 4.3 compiles on 4.4 unchanged. On a persistent store with the execution history on, run
4.4/add_execution_outcome_<dialect>.sql before the first 4.4 node starts. Every change, and the full list of
mixed-version notes, is in the migration guide's
Upgrading from 4.3 to 4.4.
Public API
Additions only: +278 lines, none removed, and every member added to an existing interface has a default implementation.
| Assembly | Lines added |
|---|---|
| Quartz | 211 |
| Quartz.Dashboard | 20 |
| Quartz.Weasel.MySQL (new) | 16 |
| Quartz.Weasel.Firebird (new) | 14 |
| Quartz.Weasel.Oracle (new) | 13 |
| Quartz.HttpClient | 2 |
| Quartz.Jobs | 2 |
The whole delta, member by member: git diff v4.3.0 v4.4.0 -- src/Quartz.Tests.Unit/Verify src/Quartz.Tests.AspNetCore/Verify.
Measured
PostgreSQL 15.1 in Docker (fsync and synchronous commit on), pool 10, three alternating sittings of 4.3.0 and 4.4.0 on one quiet machine.
| Measure | 4.3.0 | 4.4.0 |
|---|---|---|
| One-off throughput, defaults | 3.67–3.88 ms a firing (258–272/s) | 2.33–2.38 ms a firing (420–429/s) |
| Allocated per one-off firing | 72.1 KB | 60.8 KB |
| Statements per firing, history off | 16.5 | 13.8 |
| 20 triggers/s, within ±50 ms (max deviation) | 53.8–54.3 % (78–83 ms) | 95.5–99.2 % (51.5–54.2 ms) |
| 100 triggers/s, within ±250 ms | 68.2–70.3 % | 99.5–100 % |
| Clustered drain, 2 nodes | 293–298/s | 456–491/s |
| Clustered drain, 4 nodes | 287–301/s | 379–504/s |
| In-memory S1 / S4 | — | not worse; fire-ahead punctuality max 1 s → 15 ms |
- With execution history on and every firing hitting one job, history adds about 7 statements and 2.75 commits a firing and costs about 37 % of one-off throughput (235–239/s), because each completion updates that job's status row in the same transaction. Spread across jobs the cost is far smaller. Tracked in [#3979].
- A 30-minute two-node soak on durable PostgreSQL ran 6,505 executions with no duplicate firing, no overlap of a serial job, every one-off exactly once, and one elected node sweeping the history.
Dependencies
- The Quartz.Weasel packages depend on Weasel
[9.39.0, 10.0.0), which needs JasperFx 2.76.0 or later. (#4007) - Three new packages:
Quartz.Weasel.MySQL,Quartz.Weasel.OracleandQuartz.Weasel.Firebird. The package count goes from 18 to 21. (#3987, [#4005]) - coverlet 10.1.0. (#3947)
- WolverineFx 6.41.0, the four Weasel floors held so they move together, and nine unreferenced package rows removed. (#3969, closes [#3966])
Also
- Tests run on NUnit 5 and Microsoft.Testing.Platform (#3976, closes [#3970], supersedes [#3943]; prepared in [#3969]).
Contributors run
dotnet test --project …. - Rider keeps working through
Microsoft.NET.Test.Sdk(RIDER-131530), and a test guardsglobal.json's runner (RIDER-140778). - GitHub annotations and summaries go through
--report-github. - Coverage goes through coverlet.MTP (same 16 modules).
- HangDump/CrashDump replace blame.
- The thread-pool allocation test measures on one thread.
- The retired GitHub Models issue-triage workflow is deleted, and the comparison page reads 4.4. (#3969)