| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| Quartz.NET-3.18.0.zip | 2026-04-11 | 45.5 MB | |
| README.md | 2026-04-11 | 12.2 kB | |
| v3.18.0 source code.tar.gz | 2026-04-11 | 6.0 MB | |
| v3.18.0 source code.zip | 2026-04-11 | 6.7 MB | |
| Totals: 4 Items | 58.2 MB | 0 | |
Quartz.NET 3.18.0 is a major feature release with 8 new capabilities, performance improvements, and important bug fixes.
New Features
RFC 5545 RRULE recurrence trigger
Quartz.NET now supports scheduling with iCalendar recurrence rules (RFC 5545 RRULE), enabling complex patterns that cannot be expressed with cron expressions — such as "2nd Monday of every month" or "last weekday of March each year."
A custom lightweight RRULE engine (~1.5K LOC) handles all frequencies (YEARLY through SECONDLY), all BY* rules (BYDAY, BYMONTHDAY, BYSETPOS, etc.), COUNT, UNTIL, INTERVAL, and WKST. No new external dependencies. No database schema changes — uses the existing SIMPROP_TRIGGERS table.
:::csharp
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger")
.WithRecurrenceSchedule("FREQ=MONTHLY;BYDAY=2MO", b => b
.InTimeZone(TimeZoneInfo.FindSystemTimeZoneById("America/New_York")))
.StartNow()
.Build();
| RRULE | Pattern |
|---|---|
FREQ=MONTHLY;BYDAY=2MO |
Every 2nd Monday of the month |
FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR |
Every other week on Mon/Wed/Fri |
FREQ=YEARLY;BYMONTH=3;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 |
Last weekday of March each year |
FREQ=MONTHLY;BYMONTHDAY=-1 |
Last day of every month |
FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR |
Every weekday |
(#2990) — Closes [#1259]
Execution groups for per-node thread limits
Tag triggers with an execution group to limit how many threads a category of jobs can consume concurrently on each node. This prevents resource-intensive jobs from starving lightweight work.
:::csharp
services.AddQuartz(q =>
{
q.UseExecutionLimits(limits =>
{
limits.ForGroup("batch-jobs", maxConcurrent: 2);
limits.ForDefaultGroup(maxConcurrent: 10);
limits.ForOtherGroups(maxConcurrent: 5);
});
q.AddTrigger(t => t
.ForJob("heavyJob")
.WithExecutionGroup("batch-jobs")
.WithCronSchedule("0 0/5 * * * ?"));
});
Also configurable via properties (quartz.executionLimit.batch-jobs = 2) and runtime API (scheduler.SetExecutionLimits(...)). Includes optional EXECUTION_GROUP column for ADO.NET job stores with graceful fallback when absent, and Dashboard integration.
(#3004) — Closes [#1175], [#830]
Multiple named schedulers in Microsoft DI
Register multiple independent scheduler instances in a single DI container. Each named scheduler gets isolated options, jobs, triggers, listeners, and calendars.
:::csharp
services.AddQuartz("Scheduler1", q =>
{
q.AddJob<EmailJob>(j => j.WithIdentity("email"));
q.AddTrigger(t => t.ForJob("email").WithCronSchedule("0 0/5 * * * ?"));
});
services.AddQuartz("Scheduler2", q =>
{
q.UsePersistentStore(s => { /* ... */ });
q.AddJob<ReportJob>(j => j.WithIdentity("report"));
q.AddTrigger(t => t.ForJob("report").WithCronSchedule("0 0 * * * ?"));
});
services.AddQuartzHostedService(o => o.WaitForJobsToComplete = true);
AddQuartzHostedService() automatically manages the lifecycle of all registered schedulers.
(#3000) — Closes [#2109]
JSON configuration and scheduling data
Configure Quartz.NET using hierarchical JSON in appsettings.json instead of flat property keys. Supports declarative job and trigger definitions, all 4 trigger types, and named schedulers via a Schedulers section.
:::json
{
"Quartz": {
"Scheduler": { "InstanceName": "My Scheduler" },
"ThreadPool": { "MaxConcurrency": 10 },
"Schedule": {
"Jobs": [{ "Name": "myJob", "JobType": "MyApp.Jobs.MyJob, MyApp", "Durable": true }],
"Triggers": [{ "Name": "myTrigger", "JobName": "myJob", "Cron": { "Expression": "0/30 * * * * ?" } }]
}
}
}
:::csharp
services.AddQuartz(Configuration.GetSection("Quartz"));
Also includes a standalone JsonSchedulingDataProcessorPlugin for quartz_jobs.json file support with hot-reload, mirroring XMLSchedulingDataProcessorPlugin.
(#3012, #3015, #3017) — Closes [#1755]
Redis-based distributed lock handler (new Quartz.Redis package)
New Quartz.Redis NuGet package providing RedisSemaphore — an ISemaphore implementation using Redis SET NX PX distributed locks instead of database row locks. This eliminates DB row lock contention and deadlocks in clustered setups while keeping job/trigger data in the relational database.
Uses two-tier locking: local SemaphoreSlim prevents redundant Redis round-trips within the same process, and a Lua script ensures atomic check-and-delete on release for safety.
:::csharp
services.AddQuartz(q =>
{
q.UsePersistentStore(store =>
{
store.UseRedisLockHandler(redis =>
{
redis.RedisConfiguration = "redis-server:6379";
});
});
});
(#2999) — Closes [#1625]
Activity tracing for ADO.NET job store operations
28 IJobStore methods are now wrapped with System.Diagnostics.Activity spans, so database calls appear as children of named Quartz operations (e.g., Quartz.JobStore.AcquireNextTriggers) instead of orphaned root spans in your tracing system. Zero overhead when tracing is disabled.
[Quartz.JobStore.AcquireNextTriggers]
└── [SELECT ... FROM TRIGGERS]
└── [INSERT ... INTO FIRED_TRIGGERS]
[Quartz.JobStore.TriggersFired]
└── [UPDATE ... TRIGGERS SET STATE=...]
[Quartz.Job.Execute]
└── [user job work]
All new operations are automatically included in QuartzInstrumentationOptions.DefaultTracedOperations.
(#3001) — Closes [#2721]
UpdateTriggerDetails — update trigger metadata without rescheduling
New UpdateTriggerDetails method updates Description, Priority, JobDataMap, CalendarName, and MisfireInstruction on an existing trigger without resetting fire times, trigger state, or misfire context. Available via IScheduler.UpdateTriggerDetails() extension method.
(#2988) — Closes [#844]
Factory-based AddQuartz() with IServiceProvider access
New AddQuartz overloads accepting Action<IServiceCollectionQuartzConfigurator, IServiceProvider> allow resolving DI services during Quartz configuration — useful for obtaining connection strings, feature flags, or other configuration from DI-registered services.
:::csharp
services.AddQuartz((q, sp) =>
{
var config = sp.GetRequiredService<DatabaseConfig>();
q.UsePersistentStore(s =>
{
s.UseSqlServer(sql => sql.ConnectionString = config.ConnectionString);
});
});
(#3007) — Closes [#1617]
Performance
Reduced DB round-trips in misfire recovery
Misfire recovery now uses a targeted UPDATE instead of routing each trigger through the full StoreTrigger path, reducing per-trigger DB round-trips from 7-12 down to 1-2 (~87% reduction). For a batch of 20 cron triggers, this drops from ~150 queries to ~20 queries — all under LockTriggerAccess. Calendar lookups are also cached across the batch.
(#2993) — Closes [#758]
Bug Fixes
- Fix scheduler signal loss causing triggers stuck in WAITING state —
SemaphoreSlim(0, 1)inQuartzSchedulerThreadcould silently drop scheduling signals viaSemaphoreFullException, causing triggers to stay stuck in WAITING state until the next idle loop timeout. (#3033) — Fixes [#3028] - Fix SchedulerRepository preventing connections to multiple cluster nodes —
SchedulerRepositoryindexed by scheduler name only, preventing multiple remote proxies to different nodes in the same cluster from coexisting. Now supports instance-aware lookup. (#2991) — Fixes [#388] - Implement
IsJobGroupPaused/IsTriggerGroupPausedin ADO.NET job store — These methods previously threwNotImplementedExceptionin the persistent job store. (#3030) - Fix Dashboard Live not working over HTTP — Dashboard live updates now work correctly over plain HTTP connections. (#3032)
- Fix dashboard plugins using static
ServiceProvider— Dashboard plugins no longer rely on a staticServiceProviderreference, fixing issues with multiple host instances. (#3035) — Fixes [#3026]
Deprecations
DirtyFlagMap.Get()— use the indexer (map[key]) instead (#2986)DirtyFlagMap.Put()/PutAll()— use the indexer or collection initializer instead (#2989)
What's Changed
- Improve DirtyFlagMap dirty flag correctness and mark Get() obsolete by @lahma in https://github.com/quartznet/quartznet/pull/2986
- Mark Put/PutAll as [Obsolete] and migrate internal callers to indexer by @lahma in https://github.com/quartznet/quartznet/pull/2989
- Fix SchedulerRepository preventing connections to multiple cluster nodes by @lahma in https://github.com/quartznet/quartznet/pull/2991
- Reduce DB round-trips in misfire recovery (#758) by @lahma in https://github.com/quartznet/quartznet/pull/2993
- Fix misfire optimization: blob triggers, naming, docs by @lahma in https://github.com/quartznet/quartznet/pull/2995
- Add RFC 5545 RRULE recurrence trigger support by @lahma in https://github.com/quartznet/quartznet/pull/2990
- Add UpdateTriggerDetails to update trigger metadata without rescheduling by @lahma in https://github.com/quartznet/quartznet/pull/2988
- Add support for multiple named schedulers in Microsoft DI by @lahma in https://github.com/quartznet/quartznet/pull/3000
- Add Redis-based distributed lock handler (Quartz.Redis) by @lahma in https://github.com/quartznet/quartznet/pull/2999
- Add Activity tracing for ADO.NET job store operations by @lahma in https://github.com/quartznet/quartznet/pull/3001
- Add execution groups for per-node thread limits by @lahma in https://github.com/quartznet/quartznet/pull/3004
- Backport execution group fixes from 4.x port by @lahma in https://github.com/quartznet/quartznet/pull/3009
- Add factory-based AddQuartz() overload with IServiceProvider access by @lahma in https://github.com/quartznet/quartznet/pull/3007
- Add JSON configuration and scheduling data support by @lahma in https://github.com/quartznet/quartznet/pull/3012
- Backport 4.x review fixes to 3.x JSON configuration by @lahma in https://github.com/quartznet/quartznet/pull/3015
- Fix JSON scheduling bugs and add ExecutionGroup JSON/serialization support by @lahma in https://github.com/quartznet/quartznet/pull/3017
- Implement IsJobGroupPaused/IsTriggerGroupPaused in ADO.NET job store by @lahma in https://github.com/quartznet/quartznet/pull/3030
- Fix Dashboard Live not working over HTTP by @lahma in https://github.com/quartznet/quartznet/pull/3032
- Fix scheduler signal loss causing triggers stuck in WAITING state by @lahma in https://github.com/quartznet/quartznet/pull/3033
- Fix dashboard plugins using static ServiceProvider (#3026) by @lahma in https://github.com/quartznet/quartznet/pull/3035
Full Changelog: https://github.com/quartznet/quartznet/compare/v3.17.1...v3.18.0