Download Latest Version Quartz.NET-4.5.1.zip (52.0 MB) Google Add to Preferred Sources
Home / v4.4.0
Name Modified Size InfoDownloads / 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.Oracle and Quartz.Weasel.Firebird join 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) and MaxEntriesPerJob.
  • 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 JobExecuted event 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 a RequiredJobs list 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_CONT on 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.RecordInput records a run's JobInput, up to MaxInputBytes (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_INPUT and JOB_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.duration gains a quartz.job.result tag: succeeded, failed, cancelled or skipped, classified by the same rule as the history row. error.type is unchanged: it is set on a failed run that threw. (#4004, closes #3967)
  • The Quartz.Job.Execute span gets quartz.job.result too. A failed store span now carries error.type. (#4020, closes #4016)
  • New QuartzSchedulerOptions.RecordExceptionSpanEvents, since OpenTelemetry deprecated span events in 2026. Unset, Quartz follows OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN, so logs turns 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), plus q.UseDefaultRetryPolicy(…).
  • Precedence: the trigger's own policy, then the job's attribute, then the scheduler default. RetryPolicy.None refuses 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).
  • SendMailJob and NativeJob carry [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.
  • @reboot is still rejected, and its message names RunAtStartup.
  • Schedule a trigger already paused, with a reason (#4023, closes [#4018]).
  • ScheduleJobOptions/AddTriggerOptions gain Paused, PauseReason and PauseRequestedBy. The shipped stores write the trigger paused in the same write, so it never fires first.
  • A custom store answers through the new IJobStore.SupportsStoringPaused default 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 / PauseJobsWith on IScheduler, IJobStore and IQuartzApiClient.
  • One lock and one transaction in the database store.
  • The existing /keys/pause routes 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 IJobStore keeps the old two-step path through the new AcquireNextTriggersAndFireDue default member.

  • A custom StdAdoDelegate keeps every override it has. The new IDriverDelegate.SupportsFireOnAcquire answers 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 (key quartz.jobStore.recoverFiringsCancelledByShutdown).
  • A RequestsRecovery job 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 Interrupt or [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 key quartz.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.MaxConsecutiveFireFailures failures 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, with JobPersistenceException.

Databases and Weasel

  • Quartz.Weasel.MySQL and Quartz.Weasel.Oracle (#3987, closes [#3954] and [#3955]): UseWeaselForMySql() and UseWeaselForOracle().
  • MySQL: names are qualified by the connection's database. The migration takes a GET_LOCK user lock, which needs no privilege.
  • Oracle: names are qualified by CURRENT_SCHEMA. There is no lock, because DBMS_LOCK needs a grant. A conflicting apply re-reads and retries, as ProvisionSchema() 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. MaxIdentifierLength allows 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_ST on 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 SchedulerException with 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 reasons names 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 asserts blazor.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 Vetoed in 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.Oracle and Quartz.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 guards global.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)
Source: README.md, updated 2026-10-06