Quartz.NETQuartz.NET
Home
Features
Discussions
NuGet
GitHub
Home
Features
Discussions
NuGet
GitHub
  • 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
    • Frequently Asked Questions
    • Best Practices
    • Troubleshooting
    • API Documentation
    • Database Schema
    • 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
  • Unreleased Releases

    • Quartz 4.x
      • Quartz 4 Quick Start
      • Tutorial
        • Using Quartz
        • Jobs And Triggers
        • More About Jobs & JobDetails
        • More About Triggers
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
        • Miscellaneous Features
        • CronTrigger Tutorial
      • Configuration Reference
      • JSON Configuration
      • Migration Guide
      • Troubleshooting
      • API Documentation
      • How To's

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

        • Quartz Core Additions

          • Dashboard
          • Jobs
          • JSON Serialization
          • Plugins
        • Integrations

          • ASP.NET Core Integration
          • HTTP API
          • 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

Node affinity lets you control which cluster node runs a specific trigger. This is useful when a job maintains in-memory state (such as a cache or a warmed-up connection) between runs and should keep executing on the same node.

See also Execution Groups, which limits how many threads a category of job may use on a node. The two features compose: affinity decides where a trigger runs, execution groups decide how much of a node it may consume.

Concepts

A preferred node is an optional property of a trigger naming the scheduler instance that should acquire it. Because the setting lives on the trigger rather than the job, a job with several triggers could in principle have different preferred nodes — set the same value on all of a job's triggers if you want job-level affinity.

  • A specific scheduler instance id (e.g. "node-1", matching quartz.scheduler.instanceId) pins the trigger to that node.
  • The sentinel "*" requests auto-pin: the first node to fire the trigger claims it.
  • null (the default) means no preference — standard Quartz behavior.

Preferred node is a strong preference with automatic failover, not a hard constraint. Acquisition filters out triggers pinned to live nodes, but if the pinned node is not currently checking in, other nodes take over. See Failover behavior.

Two columns back this on QRTZ_TRIGGERS:

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 is recorded separately, so no instance id is reserved — a node may legitimately be called auto:thing or *-west without confusing Quartz.

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 live there rather than 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. Pin comparisons happen in SQL using the database's string collation, so a value differing only in case is a different node — and on a case-sensitive database, one that never matches.

Auto-pin mode

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

  1. The trigger is acquirable by any node, 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 rather than being clobbered by it.
  3. From then on only that node acquires the trigger — until it stops checking in.

This is ideal when you don't know node names at configuration time but still want a trigger to stay put.

Rebuilding an auto-pinned trigger preserves 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, since it expresses your intent rather than a claim Quartz made:

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 may acquire its pinned triggers immediately — without waiting for cluster recovery.
  2. Steal on fire. A node that fires a trigger still auto-claimed by another (stale) node takes the pin over via compare-and-swap. Affinity converges on a live node instead of bouncing.
  3. Cluster recovery. When recovery confirms a node dead, auto-claimed pins belonging to it are reset to "*" before its state row is deleted, so any eligible node can claim them — which correctly respects execution group limits.
  4. Explicit pins are preserved. They are never re-pinned. While the node is down other nodes run the trigger; when it returns and checks in again, it naturally reclaims it.

Updating the preferred node at runtime

You can re-pin without rescheduling:

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

Pass null to clear the preference entirely:

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

Requirements and limitations

  • Clustering and a stable instance id. Affinity only means anything with quartz.jobStore.clustered = true and a stable quartz.scheduler.instanceId. With AUTO, the id changes on every restart and a stored pin refers to 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 (by overriding SelectTriggerToAcquire or the GetSelectNextTriggerToAcquire*Sql builders) but does not extend the preferred-node variants, Quartz keeps acquisition on that delegate's own path and logs a warning: the custom SQL keeps working, but SQL-level affinity filtering is not applied for it.
  • Pinned to a node that never registers. If the target instance id has never checked in, the trigger is eligible everywhere. Affinity is a preference, not a guarantee, so verify the id is spelled right.
  • A live but saturated node still holds its pin. If the pinned node is up but its execution group is at its limit, the trigger waits for that node rather than moving. Failover reacts to node death, not to node busyness.
  • Brief spread during failover. Between a node dying and ownership settling, a fast-firing trigger may run on more than one surviving node before converging.

Schema

PREFERRED_NODE and PREFERRED_NODE_AUTO are optional columns on QRTZ_TRIGGERS in 3.x. Add them with database/schema_30_add_preferred_node.sql (both columns must be added together).

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

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

Help us by improving this page!
Last Updated: 7/23/26, 7:38 AM
Contributors: Marko Lahma, Claude Opus 4.8 (1M context)
Prev
Execution Groups
Next
Simple Triggers