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
    • 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
    • Retrying Failed Jobs
    • Job Continuations
    • 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
    • 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 runs schedulers as a hosted service, started and stopped with the application.

Installation

The hosted service is in the core package. The worker and web templates already reference the host; a plain console project needs it added:

dotnet add package Quartz
dotnet add package Microsoft.Extensions.Hosting

Using

Call AddQuartzHostedService on the host application builder or on IServiceCollection. Configuring the scheduler, jobs and triggers is covered in Microsoft DI Integration; several schedulers in one application in Multiple Schedulers.

  • The hosted service starts every scheduler in the container.
  • It resolves them when the host starts, so AddQuartz and AddQuartzHostedService can be called in either order.
  • Options apply to every scheduler. Override one by name with AddQuartzHostedService("SchedulerName", options => …).

Warning

AddQuartzHostedService() with no scheduler registered throws at startup. Register one with AddQuartz(...).

Example program utilizing hosted services configuration

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// see Quartz documentation about how to configure different configuration aspects
builder.AddQuartz(q =>
{
    // your configuration here
});

// Quartz hosting
builder.AddQuartzHostedService(options =>
{
    // when shutting down we want jobs to complete gracefully
    options.WaitForJobsToComplete = true;
});

await builder.Build().RunAsync();

Options

QuartzHostedServiceOptions:

OptionDefaultDescription
WaitForJobsToCompletefalseShutdown waits for running jobs to finish
AwaitApplicationStartedtrueScheduler starts only after application startup completes
StartDelaynoneExtra delay before start; counted from startup completion with AwaitApplicationStarted
AutoStarttruefalse: built, initialized and bound, but not started; see below

Without WaitForJobsToComplete, the host stops while jobs are still running. Whether running jobs are also asked to stop is the scheduler's ShutdownJobInterruption setting.

To run code around the lifecycle (a warm-up before start, a drain after stop), derive from QuartzHostedService and register the subclass. StartingAsync, StartedAsync, StoppingAsync and StoppedAsync are virtual; Schedulers lists the schedulers it runs.

builder.AddQuartzHostedService<WarmUpBeforeSchedulingService>(options => options.WaitForJobsToComplete = true);

builder.AddQuartz(...) is builder.Services.AddQuartz(...) that also reads the Quartz configuration section, so appsettings.json settings apply before your callback. For a section with another name, use the IServiceCollection overload:

builder.Services.AddQuartz(builder.Configuration.GetSection("Scheduling"), q => { });

A string argument is a scheduler name. builder.AddQuartz("reporting", …) registers scheduler reporting and reads Quartz:Schedulers:reporting when the section describes several. builder.AddQuartzSchedulers() registers one per child of that sub-section.

A scheduler the application starts itself

Set AutoStart = false when the application must start the scheduler itself: after its own leader election, after a message bus connects, or when a module comes up late.

builder.AddQuartz("reporting", q => { });

// Built, initialized and bound with the host, but left in Created for the application to start
builder.AddQuartzHostedService("reporting", options => options.AutoStart = false);
  • The scheduler is resolved, initialized and bound when the host starts. ISchedulerRegistry, the dashboard and GET /schedulers see it.
  • It stays in Created until something calls scheduler.Start().
  • AutoStart overrides AwaitApplicationStarted and StartDelay, which then do not apply.
  • Shutdown is unchanged: the hosted service shuts down every scheduler it created, started or not. Not registering the hosted service at all would lose that shutdown handling.
  • It is per scheduler: the example defers reporting and leaves the others to start with the host.

Health checks

The scheduler health check is in the core Quartz package and registers on the standard IHealthChecksBuilder, so a worker with no web stack can use it.

StatusWhen
HealthyRunning and can reach its store
DegradedIn standby, or in Created with AutoStart = false
UnhealthyAnything else
builder.Services.AddHealthChecks().AddQuartz();

AddQuartz("reporting", q => q.AddQuartzHealthChecks()) registers the same check from a named scheduler's builder, without repeating the name.

The check reads each scheduler's own QuartzHostedServiceOptions. A scheduler in Created that did not opt out of AutoStart is unhealthy, including in an application with no hosted service, where nothing will start it.

Customize the registration in the callback, for example with tags for separate liveness and readiness probes:

builder.Services.AddHealthChecks().AddQuartz(options =>
{
    options.Name = "quartz-scheduler";   // the default, or quartz-scheduler-<name> for a named scheduler
    options.Tags.AddRange(["ready", "live"]);
    options.FailureStatus = HealthStatus.Unhealthy;

    // What a scheduler in standby reports. Degraded by default; say Unhealthy where a standby
    // node must leave the rotation and there is no HTTP probe to remap the status code at.
    options.StandbyStatus = HealthStatus.Unhealthy;
});

QuartzHealthCheckOptions go through the options pipeline: the callback, services.Configure<QuartzHealthCheckOptions>(...) and a bound configuration section are equivalent, in any order.

OptionMeaning
Namequartz-scheduler, or quartz-scheduler-<name> for a named scheduler
TagsTags for filtering into probes
FailureStatusWhat the registration reports when the check fails
StandbyStatusWhat a scheduler in standby reports; default degraded
StaleFiringToleranceOverdue-trigger detection; default off (null); see below

Degraded answers HTTP 200, and a worker project has no endpoint to remap it. Set StandbyStatus = HealthStatus.Unhealthy if standby nodes must leave the rotation. It covers standby only: a scheduler in Created because AutoStart is false still reports degraded.

A named scheduler has its own check. Register it on the health checks builder or inside AddQuartz:

builder.Services.AddHealthChecks().AddQuartz("reporting", options => options.Tags.Add("ready"));

// or, where the scheduler is configured
builder.Services.AddQuartz("reporting", q => q.AddQuartzHealthChecks());

Configure its options under its name:

builder.Services.Configure<QuartzHealthCheckOptions>("reporting", options => options.Tags.Add("ready"));

Serving the report over HTTP (MapHealthChecks) and how degraded maps to a status code are in ASP.NET Core Integration.

Saying that a scheduler has stopped firing

The checks above test reachability, not progress. A scheduler with a wedged thread, a full thread pool or an unreleased store lock still reports Running, answers store queries and checks in to its cluster, while firing nothing. Set StaleFiringTolerance to detect this:

builder.Services.AddHealthChecks().AddQuartz(options =>
{
    // Degraded once a trigger is three misfire thresholds overdue, unhealthy at six.
    // Off (null) by default: what counts as overdue is the application's to say.
    options.StaleFiringTolerance = 3;
});
  • The check looks for a schedulable trigger whose fire time is overdue by more than StaleFiringTolerance misfire thresholds: AdoJobStoreOptions.MisfireThreshold or InMemoryJobStoreOptions.MisfireThreshold, or one minute for a custom store that exposes neither.
  • Overdue by that much: degraded. Overdue by twice that: unhealthy.
  • The report data carries overdueTrigger, overdueSince and overdueBy.
  • Start with 3, as for ClusterCheckinTolerance. A trigger can be one threshold late before it counts as misfired, and one more sweep late before the misfire handler (whose default interval on the database store is the misfire threshold) reaches it, so the value must be more than one.
  • Off by default: a scheduler working down a backlog after a maintenance window is behind but healthy.
  • Not evaluated for a scheduler in standby; that verdict comes before the store is queried.
  • A scheduler with all triggers paused has nothing schedulable, so it never counts as stalled.
  • Left unset, the check costs what it did before.

Shutdown has a budget

The host's StopAsync token fires after HostOptions.ShutdownTimeout (default thirty seconds) and bounds the wait for running jobs.

  • When it fires, schedulers stop waiting. They still shut down job stores, plugins and listeners, and listeners are still notified. A warning naming the scheduler is logged.
  • The deadline does not cancel jobs. QuartzSchedulerOptions.ShutdownJobInterruption (default: never) decides whether a shutting-down scheduler asks them to stop; a job that must end on request watches IJobExecutionContext.CancellationToken.
  • With WaitForJobsToComplete = true and jobs that outlive the budget, the host stops with those jobs running and their job store updates unfinished. Raise HostOptions.ShutdownTimeout if that matters more than a prompt stop.
  • Several schedulers shut down in parallel and share one budget.
Help us by improving this page!
Last Updated: 9/24/26, 8:07 PM
Contributors: Marko Lahma, Claude Opus 5.5 (1M context)
Prev
Dashboard
Next
Microsoft DI Integration