Download Latest Version Quartz.NET-3.18.2.zip (48.6 MB)
Email in envelope

Get an email when there's a new version of Quartz.NET

Home / v3.18.0
Name Modified Size InfoDownloads / 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 stateSemaphoreSlim(0, 1) in QuartzSchedulerThread could silently drop scheduling signals via SemaphoreFullException, causing triggers to stay stuck in WAITING state until the next idle loop timeout. (#3033) — Fixes [#3028]
  • Fix SchedulerRepository preventing connections to multiple cluster nodesSchedulerRepository indexed 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/IsTriggerGroupPaused in ADO.NET job store — These methods previously threw NotImplementedException in 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 static ServiceProvider reference, 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

Full Changelog: https://github.com/quartznet/quartznet/compare/v3.17.1...v3.18.0

Source: README.md, updated 2026-04-11