Quartz.NETQuartz.NET
Home
Features
Blog
Discussions
NuGet
GitHub
Home
Features
Blog
Discussions
NuGet
GitHub
  • Getting Started

    • Overview
    • Quartz 4 Quick Start
    • Tutorial
      • Using Quartz
      • Jobs And Triggers
      • More About Jobs & JobDetails
      • Job Data
      • More About Triggers
      • Querying Jobs and Triggers
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Time and TimeProvider
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Execution Middleware
      • Job Stores
      • Configuration, Resource Usage and Building a Scheduler
      • Building a Scheduler Without a Host
      • Clustering
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Testing
      • Compile-Time Checks
      • Declaring Jobs with Attributes
      • Delegate Jobs
    • Configuration Reference
    • JSON Configuration
    • Cron Expression Reference
    • Multi-Tenancy
    • Comparison
    • Frequently Asked Questions
    • Best Practices
    • Before You Go Live
    • Operating a Cluster
    • Log Events
    • Tenancy Patterns
    • Database Schema
    • Database Schema Changes
    • Migration Guide
    • Troubleshooting
    • API Documentation
  • How To's
    • One-Off Job
    • Rescheduling Jobs
    • Backfill
    • Retrying Failed Jobs
    • Pausing with a Reason
    • Job Continuations
    • Overlap Policy
    • Progress and Execution Logs
    • Job Outcomes
    • Multiple Triggers
    • Job Template
    • Running Quartz under Aspire
    • Quartz.NET with Wolverine
    • Coming from Hangfire
    • Coming from TickerQ
    • Embedding Quartz in a Library
    • Running under an External Leader Election
    • Publishing Trimmed and Native AOT
    • Extending Quartz: what is open, what is closed, and how to ask
    • A Job Store of Your Own
    • A Driver Delegate for a New Database
    • Persisting a Custom Trigger Type
    • A Lock Handler of Your Own
  • Packages

    • Quartz Core Additions

      • Jobs
      • Serialization (System.Text.Json)
      • JSON Serialization
      • Plugins
    • Integrations

      • Aspire Integration
      • ASP.NET Core Integration
      • HTTP API
      • HTTP Client
      • Dashboard
      • Dashboard Agent
      • Hosted Services Integration
      • Microsoft DI Integration
      • Multiple Schedulers with Microsoft DI
      • Observability
      • Redis Lock Handler
      • TimeZoneConverter Integration
      • Weasel Schema Management
    • 3rd Party Plugins for Quartz
  • Quartz 3.x

    • Getting Started

      • Quartz 3 Quick Start
      • Tutorial
        • Using Quartz
        • Library Overview
        • Jobs And Triggers
        • More About Jobs
        • More About Triggers
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Tuning the Scheduler
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
      • Configuration Reference
      • JSON Configuration
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Tenancy Patterns
      • Troubleshooting
      • API Documentation
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Miscellaneous Features
    • How To's

      • One-Off Job
      • Multiple Triggers
      • Job Template
      • Using the CronTrigger
      • Rescheduling Jobs
    • Packages

      • Quartz Core Additions

        • Dashboard
        • Jobs
        • Serialization (System.Text.Json)
        • Serialization (Newtonsoft Json.NET)
        • Plugins
      • Integrations

        • ASP.NET Core Integration
        • Hosted Services Integration
        • Microsoft DI Integration
        • Multiple Schedulers with Microsoft DI
        • OpenTelemetry Integration
        • OpenTracing Integration
        • Redis Lock Handler
        • TimeZoneConverter Integration
      • 3rd Party Plugins for Quartz
  • Old Releases

    • Quartz 2.x
      • Quartz 2 Quick Start
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
        • CronTrigger Tutorial
      • Configuration Reference
      • Migration Guide
      • API Documentation
    • Quartz 1.x
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
      • API Documentation
  • License

Quartz.NET 3.x

These are the documents for the 3.x line, which is maintained. Quartz.NET 4.x is the current release; the migration guide says what changed and what to do about it.

Node affinity sets which cluster node runs a trigger. Use it for a job that keeps in-memory state between runs, such as a cache or a warmed-up connection, and should stay on one node.

Execution Groups limit how many threads a category of job may use on a node. The two combine: affinity decides where a trigger runs, execution groups how much of a node it uses.

Concepts

A preferred node is an optional property of a trigger naming the scheduler instance that should acquire it. It is set per trigger, not per job, so for job-level affinity set the same value on all of a job's triggers.

ValueEffect
a scheduler instance id (e.g. "node-1", matching quartz.scheduler.instanceId)pins the trigger to that node
"*"auto-pin: the first node to fire the trigger claims it
null (default)no preference; standard Quartz behavior

A preferred node is a strong preference with automatic failover, not a hard constraint. Acquisition skips triggers pinned to live nodes, but if the pinned node stops checking in, other nodes take over. See Failover behavior.

Two columns on QRTZ_TRIGGERS store it:

PREFERRED_NODEPREFERRED_NODE_AUTOMeaning
NULLfalseNo affinity (default)
'*'falseAuto-pin requested, not yet claimed
'node-1'trueAuto-claimed by node-1
'node-1'falseExplicit pin to node-1

The node name is stored verbatim and the auto-claim separately, so no instance id is reserved: a node may be called auto:thing or *-west.

Setting the preferred node

Use TriggerBuilder.WithPreferredNode():

// Pin to a specific node
ITrigger trigger = TriggerBuilder.Create()
    .WithIdentity("myTrigger")
    .ForJob(job)
    .WithPreferredNode("production-node-1")
    .WithCronSchedule("0 0/5 * * * ?")
    .Build();
// Auto-pin: the first node to fire it claims it
ITrigger trigger = TriggerBuilder.Create()
    .WithIdentity("myTrigger")
    .ForJob(job)
    .WithPreferredNode("*")
    .WithCronSchedule("0 0/5 * * * ?")
    .Build();

Read it back by casting to AbstractTrigger. In 3.x the properties are there, not on ITrigger, which cannot gain members without a breaking change:

ITrigger t = await scheduler.GetTrigger(new TriggerKey("myTrigger"));
var at = (AbstractTrigger) t;
string? node = at.PreferredNode;      // "production-node-1"
bool auto = at.IsPreferredNodeAuto;   // false for an explicit pin

Warning

The value must match the instance id exactly. Pins are compared in SQL with the database's string collation, so a value differing only in case is a different node, and on a case-sensitive database it never matches.

Auto-pin mode

When a trigger's preferred node is "*":

  1. Any node can acquire the trigger, as usual.
  2. The first node to fire it writes its own instance id to PREFERRED_NODE and sets PREFERRED_NODE_AUTO. The write is a compare-and-swap against the value seen at acquisition, so a concurrent re-pin or clear wins over the claim.
  3. From then on only that node acquires the trigger, until it stops checking in.

Use it when you do not know node names at configuration time but want a trigger to stay put.

Rebuilding an auto-pinned trigger keeps the auto-claim:

// The rebuilt trigger is still auto-pinned, so it will still fail over if that node dies
ITrigger rebuilt = trigger.GetTriggerBuilder().WithDescription("updated").Build();

Assigning PreferredNode directly always records an explicit pin:

trigger.PreferredNode = "node-2";   // explicit pin; IsPreferredNodeAuto becomes false

Failover behavior

When the preferred node stops checking in:

  1. Acquisition. The acquisition query treats a node whose last check-in is older than the cluster check-in threshold as dead, so surviving nodes can acquire its pinned triggers at once, without waiting for cluster recovery.
  2. Steal on fire. A node that fires a trigger still auto-claimed by a stale node takes over the pin by compare-and-swap. Affinity converges on a live node instead of bouncing.
  3. Cluster recovery. When recovery confirms a node dead, its auto-claimed pins are reset to "*" before its state row is deleted. Any eligible node can then claim them, which respects execution group limits.
  4. Explicit pins are kept. They are never re-pinned. While the node is down other nodes run the trigger; when it checks in again, it reclaims it.

Updating the preferred node at runtime

Re-pin without rescheduling:

await scheduler.UpdateTriggerDetails(
    new TriggerKey("myTrigger"),
    new TriggerDetailsUpdate().WithPreferredNode("node-2"));

Pass null to clear the preference:

await scheduler.UpdateTriggerDetails(
    new TriggerKey("myTrigger"),
    new TriggerDetailsUpdate().WithPreferredNode(null));

Requirements and limitations

  • Clustering and a stable instance id. Affinity works only with quartz.jobStore.clustered = true and a stable quartz.scheduler.instanceId. With AUTO, the id changes on every restart and a stored pin names a node that no longer exists. Quartz warns at startup when it detects an auto-generated id.
  • RAMJobStore ignores it. A pin is stored and returned as metadata but never filters acquisition; a single-node in-memory scheduler always runs the trigger.
  • Custom driver delegates. If a delegate customizes trigger acquisition (overriding SelectTriggerToAcquire or the GetSelectNextTriggerToAcquire*Sql builders) without extending the preferred-node variants, Quartz keeps acquisition on the delegate's own path and logs a warning. The custom SQL keeps working, without SQL-level affinity filtering.
  • A node that never registers. If the target instance id has never checked in, the trigger is eligible everywhere. Check the id's spelling.
  • A live but saturated node keeps its pin. If the pinned node is up but its execution group is at its limit, the trigger waits for that node. Failover reacts to node death, not to load.
  • Brief spread during failover. Between a node dying and ownership settling, a fast-firing trigger may run on more than one surviving node.

Schema

PREFERRED_NODE and PREFERRED_NODE_AUTO are optional columns on QRTZ_TRIGGERS in 3.x. Add both together with the script for your database in database/migrations/3.19/. See Database Schema Changes.

Quartz probes for them at startup. Without them the feature is unavailable: the scheduler logs a warning, pins are ignored, and everything else works as before, so upgrading the assembly without touching the database is safe. If the database is unreachable at startup, the probe retries on the first successful operation.

Both columns are mandatory in 4.x, which stores pins the same way, so upgrading needs no data migration.

Help us by improving this page!
Last Updated: 10/19/26, 11:45 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Execution Groups
Next
Simple Triggers