Download Latest Version v4.1.1 source code.zip (9.1 MB) Google Add to Preferred Sources
Home / v4.1.1
Name Modified Size InfoDownloads / Week
Parent folder
README.md 2026-09-19 6.6 kB
v4.1.1 source code.tar.gz 2026-09-19 7.4 MB
v4.1.1 source code.zip 2026-09-19 9.1 MB
Totals: 3 Items   16.5 MB 8

Quartz.NET 4.1.1 is a maintenance release with one real bug in it: a scheduler that published traces produced one trace per process rather than one per firing, and it grew for as long as the process lived. Nothing about how a trigger fires changed, the schema is 4.0's, and the public API gained two overloads and lost nothing.

:::shell
dotnet add package Quartz --version 4.1.1

What changed

  • A firing is a trace of its own again — subscribing to ActivitySource("Quartz") gave a single unbounded trace: every span the scheduler's loop opened became the parent of the next one, and each Quartz.Job.Execute hung off whichever was current when it was dispatched. A day of a quiet staging pod arrived at the backend as two trace ids and a tree several thousand spans deep, with the jobs' own EF Core and HttpClient spans buried under thousands of Quartz.JobStore.AcquireNextTriggers. The documentation has promised the opposite since 4.0 — the firing is its own trace root — and nothing tested it, because the observability suite reads tags off one span at a time and a span's parent is not a tag.

Two independent defects made that trace, either sufficient on its own. The store decorator started its span in the synchronous override and stopped it inside the asynchronous continuation: Activity.Current is an AsyncLocal, so the start wrote onto the caller's execution context and the stop put the parent back onto the continuation's, which is discarded — leaving the caller with the span current for good, once per iteration of a loop that lives as long as the process. And the execute span took whatever activity was ambient as its parent, so merely starting the scheduler inside a request decided every firing's trace for the life of the application.

Quartz.Job.Execute and Quartz.Job.Veto are now created with an empty parent context — so a parent-based sampler is asked about a root rather than about a context that is being discarded — with Activity.Current cleared around the start that re-reads it, and restored when the span stops. The store spans are started and stopped on one execution context. The three background loops — the scheduler thread, the cluster manager and the misfire handler — clear Activity.Current on entry, because their task captured the execution context of whoever called Start() and they outlive that call by the whole life of the process. Store spans still belong to whoever made the call, so scheduler.ScheduleJob(…) inside a request stays in that request's trace. (#3797, [#3799])

On the reporter's own repro, which expected 7 traces and 7 roots:

traces largest trace executions trace roots
4.1.0 2 30 spans 7 0
4.1.1 31 1 span 7 7
  • Quartz.Job.Veto also gained the ActivityLink back to the call that scheduled the firing, which it never carried; a refused fire is now walked back from exactly as an executed one is.
  • The link is handed to CreateActivity rather than added after it. ActivityListener.Sample runs while the activity is created, so a link added afterwards was one no sampler ever saw — and sampling a consumer span by the trace that produced it is the reason links are given to samplers at all.
  • Behavior change worth noting: this is a change in the shape of what an exporter receives. A dashboard or saved query that searched for job executions by the trace they were nested in will not find them; they are roots. This is the documented behaviour finally arriving, not a new convention.

  • An execution limit or a job timeout can be read from the container — 4.x dropped the AddQuartz overloads that handed the callback an IServiceProvider, and the replacement the migration guide led with (read IConfiguration at registration time) bypasses Configure, PostConfigure and IValidateOptions. Everything else on IQuartzBuilder either lands in named options or has a Func<IServiceProvider, …> shape; UseExecutionLimits and AddJobTimeout computed their value eagerly and registered an internal type, so neither could be reached from the container at all. Both now have a shape that is handed one when the scheduler is built. Precedence is untouched — both shapes TryAdd, so the first declaration in code wins and code still beats the quartz.executionLimit.* keys. The migration guide now leads with the options pattern rather than with reading a section yourself, and names the caveat that made the old advice misfire: a scheduler's options are configured under its own name, so a bare AddOptions<T>() silently configures nothing of a named scheduler's. 3.x is unaffected — its four (configurator, IServiceProvider) overloads are still there. (#3794, d523c898bc)

  • The documentation's dead links are fixed — the cron pages pointed at FreeFormatter.com, which is gone; they now answer with the library itself, and a handful of other rotted links across the 1.x–4.x trees were fixed with them. (#3796)

Public API — two additions, nothing else

Added What it is
QuartzBuilderExtensions.AddJobTimeout(IQuartzBuilder, Func<IServiceProvider, TimeSpan?>) the default job timeout, computed once the container exists
QuartzBuilderExtensions.UseExecutionLimits(IQuartzBuilder, Action<IServiceProvider, ExecutionLimitsBuilder>) execution-group limits, configured with services in hand

v4.1.0..v4.1.1 baseline diff: 2 added lines, 0 removed. Every package's dependency list is byte-identical to 4.1.0 — the Microsoft.Extensions.* floors stay at 10.0.0, so an application pinned a servicing patch lower still restores.

One source-level wrinkle the second overload brings: AddJobTimeout(null) written with a literal null is now ambiguous between the two shapes. It means what AddJobTimeout() means; write that. Every other spelling — a TimeSpan, a TimeSpan? variable, the named-argument form, no argument at all — is unchanged, and UseExecutionLimits(null) is unaffected.

Upgrading

From 4.1.0: dotnet add package Quartz --version 4.1.1, and nothing else. There is no schema change, no configuration change, and no code change to make.

If you worked around [#3797] by leaving AddSource("Quartz") out and opening a root in an IJobExecutionMiddleware, you can drop the middleware and subscribe normally — and keep the Quartz.JobStore.* spans, which are now short traces of their own rather than noise in yours.

Full changelog: https://github.com/quartznet/quartznet/compare/v4.1.0...v4.1.1

Source: README.md, updated 2026-09-19