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
      • 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

Rescheduling Jobs

Three operations get called rescheduling. Pick the wrong one and a trigger loses its fire history, or a priority change resets the next fire time.

You want toUseFire times
Change when the job runsRescheduleJobrecomputed from the new trigger
Change the trigger's metadataUpdateTriggerDetailspreserved
Retry this firingJobExecutionException { RefireImmediately = true }untouched

Changing the schedule: RescheduleJob

RescheduleJob deletes the old trigger and stores the new one, which must name the same job:

ITrigger replacement = TriggerBuilder.Create()
    .WithIdentity("nightly", "reports")
    .ForJob(new JobKey("build-report", "reports"))
    .WithCronSchedule("0 30 2 * * ?")
    .Build();

DateTimeOffset? firstFire = await scheduler.RescheduleJob(
    new TriggerKey("nightly", "reports"),
    replacement,
    cancellationToken);
  • A different WithIdentity renames the trigger.
  • The new trigger must carry a job key; the old one is gone before it is stored.
  • It returns the first fire time, or null if the old trigger was not found, in which case nothing is stored. To handle a missing trigger:
DateTimeOffset? next = await scheduler.RescheduleJob(key, replacement, cancellationToken);
if (next is null)
{
    // the old trigger was gone; store the new one on its own terms
    await scheduler.ScheduleJob(replacement, cancellationToken: cancellationToken);
}

Everything derived from the old trigger resets: PreviousFireTimeUtc is empty, a SimpleTrigger's repeat count starts over, and a paused trigger takes whatever state its new group implies.

A firing already running is not touched: it completes as the old trigger, and in a persistent store its fired-trigger record stays until then, so a job that requested recovery is still recovered if the node dies. This matters because AddJob and AddTrigger registrations are re-applied as a reschedule on every process start, before the scheduler starts and recovery runs. Unscheduling, by contrast, removes its executions' records, and nothing is recovered.

Changing metadata in place: UpdateTriggerDetails

UpdateTriggerDetails patches a stored trigger; fire times and state are kept (paused stays paused, due in ten minutes stays due in ten minutes):

bool applied = await scheduler.UpdateTriggerDetails(
    new TriggerKey("nightly", "reports"),
    new TriggerDetailsUpdate()
        .WithPriority(10)
        .WithDescription("moved up ahead of the invoice run"),
    cancellationToken);

TriggerDetailsUpdate is a patch: only properties you set change. WithCalendarName(null) removes the calendar; not calling WithCalendarName keeps it.

MethodChanges
WithDescription(string?)the description
WithPriority(int)acquisition priority
WithJobDataMap(JobDataMap)the trigger's job data map, wholesale
WithCalendarName(string?)the associated calendar; null or blank disassociates
WithMisfireInstruction(…)the misfire policy — five family-typed overloads
WithMisfireInstructionCode(int)the same, as a raw code
WithExecutionGroup(string?)the execution group; null removes it from every group
WithPreferredNode(PreferredNode)the cluster node pin
  • Returns true if the trigger was found and updated, false if the key names nothing.
  • A new misfire instruction applies the next time the trigger is late.
  • A new execution group applies from the next acquisition cycle; a running job keeps counting against the group it was acquired under.

Misfire instructions are validated against the trigger's family

The same code means different policies per family (1 is FireNow on a simple trigger, FireOnceNow on a cron trigger). The typed overloads carry the family, and the store rejects a mismatch:

// fine — the key resolves to a cron trigger
await scheduler.UpdateTriggerDetails(cronKey, new TriggerDetailsUpdate()
    .WithMisfireInstruction(CronTriggerMisfireInstruction.DoNothing));

// rejected — the key resolves to a cron trigger, not a simple one
await scheduler.UpdateTriggerDetails(cronKey, new TriggerDetailsUpdate()
    .WithMisfireInstruction(SimpleTriggerMisfireInstruction.FireNow));

WithMisfireInstructionCode(int), for a bare number from the wire, configuration or ITrigger.MisfireInstructionCode, skips the check. Prefer the typed overloads.

Changed in 4.x

All five schedule builders spell this WithMisfireInstruction. WithMisfireHandlingInstruction… and the MisfireInstruction.* constant class are gone. The typed enums (SimpleTriggerMisfireInstruction, CronTriggerMisfireInstruction, and the three others) are the public vocabulary.

Choosing between them

  • The schedule changed (cron expression, interval, end date): RescheduleJob. A schedule cannot be edited in place; it is the trigger.
  • Anything in the table above: UpdateTriggerDetails, one statement, no fire-time change, no rebuilt trigger.

Reading, rebuilding GetJobBuilder-style and storing back is a RescheduleJob and resets the same state.

Retrying inside the job

A job can ask to run again immediately after a transient failure:

public sealed class ImportJob(IImportService importer) : IJob
{
    public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            await importer.Run(cancellationToken);
        }
        catch (TransientImportException ex) when (context.RefireCount < 3)
        {
            throw new JobExecutionException(ex) { RefireImmediately = true };
        }
    }
}

RefireImmediately re-runs the same firing at once, on the same thread-pool slot. Guard on context.RefireCount, or a job that always fails becomes a hot loop.

For failures not worth retrying:

  • UnscheduleFiringTrigger = true removes the trigger that fired
  • UnscheduleAllTriggers = true removes every trigger of the job
throw new JobExecutionException($"account {id} no longer exists")
{
    UnscheduleAllTriggers = true,
};

Changed in 4.x

JobExecutionException has four constructors — (), (Exception), (string) and (string, Exception) — and the three flags are init-only properties rather than constructor parameters. The 3.x new JobExecutionException(msg, cause, refireImmediately) shapes are gone; write new JobExecutionException(ex) { RefireImmediately = true }.

Backoff without holding a thread

An in-job retry loop (Task.Delay, Polly, a sleeping while) holds a pool slot through the backoff: three jobs backing off for a minute on a pool of ten take a third of the scheduler. If the retry can wait, schedule a one-off trigger and return:

public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
    try
    {
        await importer.Run(cancellationToken);
    }
    catch (TransientImportException) when (context.RefireCount == 0)
    {
        ITrigger retry = TriggerBuilder.Create()
            .WithIdentity($"{context.Trigger.Key.Name}-retry-{context.FireInstanceId}", "retries")
            .ForJob(context.JobDetail.Key)
            .StartAt(DateTimeOffset.UtcNow.AddMinutes(5))
            .Build();

        await context.Scheduler.ScheduleJob(retry, cancellationToken: cancellationToken);
    }
}
  • Name each retry trigger uniquely (the fire instance id is a good suffix). A fixed name makes the second retry throw ObjectAlreadyExistsException inside the job.
  • The store removes a fired one-off trigger with no next fire time, and a non-durable job with it, so retries do not pile up.

Recovering triggers that failed

A trigger whose job failed in a way the scheduler could not handle goes to TriggerState.Error and stops. Query for them, looping over pages:

TriggerQuery broken = new() { State = TriggerState.Error, Take = 250 };

while (true)
{
    PagedResult<TriggerHeader> page = await scheduler.QueryTriggers(broken, cancellationToken);
    if (page.Items.Count == 0)
    {
        break;
    }

    List<TriggerKey> keys = page.Items.Select(h => h.Key).ToList();
    List<TriggerKey> reset = await scheduler.ResetTriggersFromErrorState(keys, cancellationToken);
    logger.LogInformation("Reset {Count} triggers", reset.Count);

    if (!page.HasMore)
    {
        break;
    }
}
  • ResetTriggerFromErrorState(key) returns true only if the trigger exists and was in error; false otherwise, the same missing-key rule as PauseTrigger, ResumeTrigger and UnscheduleJob.
  • ResetTriggersFromErrorState(keys) resets the set in one pass (one lock and transaction on the ADO store) and returns the keys it reset, in the given order; others are simply absent.
  • A reset raises no scheduler-listener event and signals nothing; the next acquisition cycle picks it up.
  • The trigger returns to Normal, or Paused if its group is paused.

Tips

Reset is not a fix. Fix the cause first (most often a job type that no longer resolves), or the trigger returns to error on its next fire.

See also

  • Job Template — the job skeleton these snippets fit into
  • Querying Jobs and Triggers — paging, filters and the counting idiom
  • More About Triggers — misfire instructions in full
Help us by improving this page!
Last Updated: 10/6/26, 2:13 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
One-Off Job
Next
Backfill