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

    • Quartz 4.x
      • Quartz 4 Quick Start
      • Tutorial
        • Using Quartz
        • Jobs And Triggers
        • More About Jobs & JobDetails
        • More About Triggers
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
        • Execution Groups
        • Node Affinity (Preferred Node)
      • Configuration Reference
      • JSON Configuration
      • Cron Expression Reference
      • Frequently Asked Questions
      • Best Practices
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Troubleshooting
      • API Documentation
      • How To's
        • One-Off Job
        • Multiple Triggers
        • Job Template
      • Packages

        • Quartz Core Additions

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

          • ASP.NET Core Integration
          • HTTP API
          • Dashboard
          • Hosted Services Integration
          • Microsoft DI Integration
          • Multiple Schedulers with Microsoft DI
          • OpenTelemetry 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 Configuration Reference

  • Scheduler
  • Thread pool
  • In-memory job store
  • Persistent job store
    • Databases
    • Locking
    • Data source
    • Clustering
  • Serialization
  • Job factory
  • Listeners, calendars and plugins
  • Several schedulers
  • Without a container
  • Legacy property keys

Quartz is configured with strongly typed options. Every option has the same name whether you set it in code or in a configuration file, so there is one vocabulary to learn rather than two:

services.AddQuartz(q => q.ConfigureScheduler(options => options.MaxBatchSize = 5));
{
  "Quartz": {
    "Scheduler": { "MaxBatchSize": 5 }
  }
}

Options are bound from the Quartz section by section name — Scheduler, ThreadPool, JobStore, DataSource — and validated at startup, so a bad value is reported against the setting that is wrong rather than failing later during scheduling.

Tips

Everything on this page can also be written as flat quartz.* keys, which earlier versions used and which Quartz still accepts. See Legacy property keys.

Scheduler

QuartzSchedulerOptions, bound from Quartz:Scheduler.

OptionTypeDefaultDescription
InstanceNamestringQuartzSchedulerDistinguishes schedulers in the same process. Every node in a cluster must share one name.
InstanceIdstringNON_CLUSTEREDMust be unique among the nodes of a cluster.
GenerateInstanceIdboolfalseDerives InstanceId at startup from the registered IInstanceIdGenerator instead of using the literal value.
ThreadNamestring{InstanceName}_QuartzSchedulerThreadName given to the scheduler's main thread.
IdleWaitTimeTimeSpan00:00:30How long to wait before re-querying the job store when nothing is due. Must be at least one second.
MaxBatchSizeint1How many triggers may be acquired at once.
BatchTriggerAcquisitionFireAheadTimeWindowTimeSpan00:00:00How far ahead of its fire time a trigger may be included in the current batch.
MakeSchedulerThreadDaemonboolfalseRuns the scheduler thread as a background thread, so it will not keep the process alive.
InterruptJobsOnShutdownboolfalseSignals cancellation to running jobs on shutdown.
InterruptJobsOnShutdownWithWaitboolfalseSignals cancellation on a shutdown that waits for jobs to finish.
ContextdictionaryemptyValues seeded into SchedulerContext.
services.AddQuartz(q => q.ConfigureScheduler(options =>
{
    options.InstanceName = "core";
    options.InstanceId = "node-1";
    options.MaxBatchSize = 5;
    options.InterruptJobsOnShutdown = true;
}));

Thread pool

ThreadPoolOptions, bound from Quartz:ThreadPool.

OptionTypeDefaultDescription
MaxConcurrencyint10How many jobs may run at once.
services.AddQuartz(q => q.UseDefaultThreadPool(maxConcurrency: 20));

To supply your own implementation:

services.AddQuartz(q => q.UseThreadPool<MyThreadPool>(options => options.MaxConcurrency = 20));

In-memory job store

InMemoryJobStoreOptions, bound from Quartz:JobStore. The in-memory store is the default and does not survive process restarts.

OptionTypeDefaultDescription
MisfireThresholdTimeSpan00:00:05How late a trigger may fire before it counts as misfired.
services.AddQuartz(q => q.UseInMemoryStore(options => options.MisfireThreshold = TimeSpan.FromSeconds(30)));

Persistent job store

AdoJobStoreOptions, bound from Quartz:JobStore. Choosing a database also selects the driver delegate that speaks its SQL dialect, so a connection string is all you normally supply:

services.AddQuartz(q => q.UsePersistentStore(store =>
{
    store.UseSqlServer(connectionString);
    store.UseSystemTextJsonSerializer();
}));
OptionTypeDefaultDescription
TablePrefixstringQRTZ_Prefix on every Quartz table name.
UsePropertiesboolfalsePersists job data as name/value strings rather than serialized objects, which keeps stored data readable and version tolerant.
MisfireThresholdTimeSpan00:01:00How late a trigger may fire before it counts as misfired.
MisfireHandlerFrequencyTimeSpan?MisfireThresholdHow often misfires are handled.
MaxMisfiresToHandleAtATimeint20How many misfired triggers are handled per pass.
DbRetryIntervalTimeSpan00:00:15How long to wait before retrying after a database failure.
MaxTransientRetriesint3How many times a transient failure such as a deadlock is retried.
TransientRetryIntervalTimeSpan00:00:01Delay between transient retries.
RetryableActionErrorLogThresholdint4How many consecutive failures before they are logged as errors.
UseDbLocksboolfalseUses database row locks. Required for clustering, and implied by UseClustering().
LockOnInsertbooltrueTakes a lock when inserting rows.
AcquireTriggersWithinLockboolfalseAcquires triggers inside the database lock.
TxIsolationLevelSerializableboolfalseUses the serializable isolation level.
AcceptEnlistedTransactionsboolfalseLets the job store use a connection the application enlisted with SchedulerEnlistmentExtensions.EnlistTransaction, so scheduling commits with the application's own work. See Joining an existing transaction.
DoubleCheckLockMisfireHandlerbooltrueRe-checks the lock before handling misfires.
MakeThreadsDaemonsboolfalseRuns the store's background threads as background threads.
PerformSchemaValidationbooltrueVerifies the expected tables exist at startup.
SelectWithLockSqlstring?noneOverrides the row-lock statement.
OpenConnectionboolfalseWhether ExternalTransactionJobStore opens the connections it creates; read only by that store.

A custom trigger persistence delegate is registered with UsePersistentStore(s => s.UseTriggerPersistenceDelegate<T>()) rather than through an option; the legacy quartz.jobStore.driverDelegateInitString key still translates to the same registrations.

Databases

MethodDatabase
UseSqlServerMicrosoft SQL Server
UsePostgresPostgreSQL
UseMySqlMySQL, using the MySql.Data driver
UseMySqlConnectorMySQL, using the MySqlConnector driver
UseOracleOracle
UseFirebirdFirebird
UseSqliteSQLite, using the Microsoft.Data.Sqlite driver
UseSystemDataSqliteSQLite, using the legacy System.Data.SQLite driver
UseGenericDatabaseAnything else, using the generic SQL dialect — and the only one that can describe its own driver

Each takes either a connection string or a callback over DataSourceOptions:

store.UseSqlServer(connectionString);
store.UseSqlServer(db => db.ConnectionStringName = "Scheduler");

Where the connection comes from is the data source's own setting, so to connect through a DbDataSource registered in the container rather than a connection string of Quartz's own, say store.UseSqlServer(db => db.UseRegisteredDataSource = true).

Describing a driver Quartz does not know

The provider name each method passes — SqlServer, Npgsql and so on — names a description of an ADO.NET driver: which connection, command and parameter types to instantiate, how parameters are named, and which enum value means "binary column". Quartz ships descriptions for the drivers of every database listed above. For anything else, describe the driver in the UseGenericDatabase call:

store.UseGenericDatabase("MyDatabase", connectionString, () => new DbMetadata
{
    ProductName = "My Database",
    AssemblyName = typeof(MyConnection).Assembly.FullName,
    ConnectionType = typeof(MyConnection),
    CommandType = typeof(MyCommand),
    ParameterType = typeof(MyParameter),
    ParameterDbType = typeof(MyDbType),
    ParameterDbTypePropertyName = nameof(MyParameter.MyDbType),
    ParameterNamePrefix = "@",
    ExceptionType = typeof(MyException),
    UseParameterNamePrefixInParameterCollection = true,
    BindByName = true,
    DbBinaryTypeName = "VarBinary",
});

There is a four-argument overload taking a DataSourceOptions callback instead of a connection string, for a driver described in code that also uses a named connection string.

A description is a registration in the container rather than process-wide state, so two containers in one process no longer have to agree on what a provider name means. Within one container a provider name means one thing, since a name is what a data source points at — two schedulers that need two different drivers give them two different names.

Describing a name Quartz already ships a description for replaces it, and a description registered in code wins over one written as quartz.dbprovider.* keys. Several drivers means several calls, one per name.

The same thing can be said as properties, which is the form 3.x used and which now arrives through IConfiguration like everything else:

{
  "Quartz": {
    "quartz.dbprovider.MyDatabase.productName": "My Database",
    "quartz.dbprovider.MyDatabase.connectionType": "MyNamespace.MyConnection, MyDriver",
    "quartz.dbprovider.MyDatabase.commandType": "MyNamespace.MyCommand, MyDriver",
    "quartz.dbprovider.MyDatabase.parameterType": "MyNamespace.MyParameter, MyDriver",
    "quartz.dbprovider.MyDatabase.parameterDbType": "MyNamespace.MyDbType, MyDriver",
    "quartz.dbprovider.MyDatabase.parameterDbTypePropertyName": "MyDbType",
    "quartz.dbprovider.MyDatabase.parameterNamePrefix": "@",
    "quartz.dbprovider.MyDatabase.exceptionType": "MyNamespace.MyException, MyDriver",
    "quartz.dbprovider.MyDatabase.useParameterNamePrefixInParameterCollection": "true",
    "quartz.dbprovider.MyDatabase.bindByName": "true",
    "quartz.dbprovider.MyDatabase.dbBinaryTypeName": "VarBinary"
  }
}

A store's data source is named after the scheduler that owns it, or quartz for the default scheduler. Connection providers are held per process, so if you run two default schedulers in one process — two standalone QuartzSchedulerBuilders against different databases — name them apart with store.UseDataSourceName("reporting-db") before choosing the database. Otherwise the second replaces the first's connection provider and both end up talking to the same database.

Locking

Leave the lock handler unset and the store chooses one for itself once it knows which database it is talking to: database row locks when clustered or when UseDbLocks is on, and an in-process monitor otherwise. UseLockHandler<T>() overrides that choice, and UseLockHandler(factory) does the same for a handler that needs building — as UseRedisLockHandler() does.

Both this and UseSerializer register against the scheduler that owns the store. Registering ISemaphore or IObjectSerializer directly against Services registers it for the container, which a named scheduler will not see.

Data source

DataSourceOptions, bound from Quartz:DataSource.

OptionTypeDescription
ProviderstringNames the description of the ADO.NET driver to use. Set for you by the database methods above; see Describing a driver Quartz does not know for a driver Quartz ships no description for.
ConnectionStringstring?The connection string. Takes precedence over ConnectionStringName.
ConnectionStringNamestring?A connection string to resolve from IConfiguration.
UseRegisteredDataSourceboolConnections come from a DbDataSource in the container. Wins over both connection string settings.

To connect through a DbDataSource registered in the container, for example by AddNpgsqlDataSource:

services.AddNpgsqlDataSource(connectionString);
services.AddQuartz(q => q.UsePersistentStore(store =>
{
    store.UsePostgres(db => db.UseRegisteredDataSource = true);
}));

There are three entry points for a data source and they say different things. UseDataSource(configure) defines one — which driver, and how to reach the database — and the database methods above are shorthands for it. UseDataSourceName(name) refers to one by name, which is how a store picks up settings registered elsewhere, such as a Quartz:DataSource:<name> section. Where the connection itself comes from is DataSourceOptions' to say, not a fourth method's.

Clustering

Clustering lets several schedulers share one database, so that if a node dies its triggers are recovered by another. Every node must use the same InstanceName and a different InstanceId.

ClusteringOptions, bound from Quartz:JobStore:Clustering. This is the only place clustering is configured: the job store reports whether it is clustered, it does not offer a second place to say so.

OptionTypeDefaultDescription
EnabledboolfalseTakes part in a cluster sharing this database. UseClustering() sets it.
CheckinIntervalTimeSpan00:00:07.5How often a node records that it is alive.
CheckinMisfireThresholdTimeSpan00:00:07.5Grace period before a node is treated as failed.
services.AddQuartz(q =>
{
    q.ConfigureScheduler(options =>
    {
        options.InstanceName = "core";
        options.GenerateInstanceId = true;
    });

    q.UsePersistentStore(store =>
    {
        store.UseSqlServer(connectionString);
        store.UseClustering(cluster =>
        {
            cluster.CheckinInterval = TimeSpan.FromSeconds(10);
            cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
        });
        store.UseSystemTextJsonSerializer();
    });
});

UseClustering() enables database locking as well, because clustering has never worked without it.

Serialization

A persistent store must be told how to serialize job data.

store.UseSystemTextJsonSerializer();
store.UseNewtonsoftJsonSerializer();   // Quartz.Serialization.Newtonsoft

Job factory

By default jobs are resolved from the container, in a scope created per firing, so a job may take scoped dependencies. To replace it:

services.AddQuartz(q => q.UseJobFactory<MyJobFactory>());

Listeners, calendars and plugins

services.AddQuartz(q =>
{
    q.AddSchedulerListener<MySchedulerListener>();
    q.AddJobListener<MyJobListener>(GroupMatcher<JobKey>.GroupEquals("reports"));
    q.AddTriggerListener<MyTriggerListener>();
    q.AddPlugin<MyPlugin>();
});

Listeners and plugins are ordinary services, so they take their dependencies through their constructors.

Several schedulers

Registering a scheduler under a name gives it its own job store, thread pool, jobs and configuration. The name is the scheduler's instance name, the key its services are registered under, and the name of its options.

services.AddQuartz("reporting", q => q.UsePersistentStore(store => store.UseSqlServer(reportingDb)));
services.AddQuartz("ingest", q => q.UseInMemoryStore());

Resolve them by name:

var reporting = await serviceProvider
    .GetRequiredKeyedService<ISchedulerFactory>("reporting")
    .GetScheduler();

In configuration, use a Schedulers section:

{
  "Quartz": {
    "Schedulers": {
      "reporting": { "ThreadPool": { "MaxConcurrency": 5 } },
      "ingest":    { "ThreadPool": { "MaxConcurrency": 20 } }
    }
  }
}

Without a container

Console applications and tests that have no host build a scheduler with QuartzSchedulerBuilder. It does not take the same configuration API — it is the configuration API: QuartzSchedulerBuilder implements IQuartzBuilder, the interface AddQuartz hands out, over a container it creates itself.

var builder = QuartzSchedulerBuilder.Create();
builder.ConfigureScheduler(options => options.InstanceName = "reporting")
    .UseDefaultThreadPool(maxConcurrency: 20)
    .UseInMemoryStore();

IScheduler scheduler = await builder.BuildScheduler();

What it adds is the two terminal methods a standalone caller needs, Build() for the factory and BuildScheduler() for the scheduler. Configuration members return IQuartzBuilder, so hold the builder in a variable and build from it, the way WebApplicationBuilder is used.

A scheduler configured entirely by flat quartz.* keys is built the same way:

IScheduler scheduler = await QuartzSchedulerBuilder.Create()
    .UseProperties(properties)
    .BuildScheduler();

UseProperties checks the keys against the ones Quartz reads, so a misspelling is reported rather than silently ignored; set quartz.checkConfiguration to false to allow keys of your own. Configuration written in code wins over the properties whichever order the two are applied in.

Legacy property keys

Earlier versions configured Quartz with flat quartz.* string keys. They still work, and mean exactly the same as the options above — they are translated into them. Both spellings of a setting always produce the same result.

Two differences are worth knowing:

  • Durations in the flat format are integer milliseconds (quartz.scheduler.idleWaitTime = 30000). As typed options they are TimeSpan ("00:00:30").
  • A .type key names an implementation. In code you select implementations with the matching Use* method instead, which is checked at compile time.
Flat keyOption
quartz.scheduler.instanceNameScheduler:InstanceName
quartz.scheduler.instanceIdScheduler:InstanceId (AUTO and SYS_PROP set GenerateInstanceId)
quartz.scheduler.threadNameScheduler:ThreadName
quartz.scheduler.idleWaitTimeScheduler:IdleWaitTime
quartz.scheduler.batchTriggerAcquisitionMaxCountScheduler:MaxBatchSize
quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindowScheduler:BatchTriggerAcquisitionFireAheadTimeWindow
quartz.scheduler.makeSchedulerThreadDaemonScheduler:MakeSchedulerThreadDaemon
quartz.scheduler.interruptJobsOnShutdownScheduler:InterruptJobsOnShutdown
quartz.scheduler.interruptJobsOnShutdownWithWaitScheduler:InterruptJobsOnShutdownWithWait
quartz.context.key.NAMEScheduler:Context:NAME
quartz.threadPool.maxConcurrency (or threadCount)ThreadPool:MaxConcurrency
quartz.threadPool.typeUseThreadPool<T>()
quartz.jobStore.typeUseInMemoryStore() / UsePersistentStore<T>()
quartz.jobStore.misfireThresholdJobStore:MisfireThreshold
quartz.jobStore.tablePrefixJobStore:TablePrefix
quartz.jobStore.usePropertiesJobStore:UseProperties
quartz.jobStore.clusteredJobStore:Clustering:Enabled, or UseClustering()
quartz.jobStore.acceptEnlistedTransactionsJobStore:AcceptEnlistedTransactions, or AcceptEnlistedTransactions()
quartz.jobStore.clusterCheckinIntervalJobStore:Clustering:CheckinInterval
quartz.jobStore.clusterCheckinMisfireThresholdJobStore:Clustering:CheckinMisfireThreshold
quartz.jobStore.dataSourceset for you by the database methods
quartz.dataSource.NAME.providerDataSource:NAME:Provider
quartz.dataSource.NAME.connectionStringDataSource:NAME:ConnectionString
quartz.dataSource.NAME.connectionStringNameDataSource:NAME:ConnectionStringName
quartz.dbprovider.NAME.*the metadata factory on UseGenericDatabase; the keys still work
quartz.serializer.typeUseSystemTextJsonSerializer() / UseNewtonsoftJsonSerializer()
quartz.plugin.NAME.typeAddPlugin<T>() or the plugin's own Use* method
quartz.jobStore.lockHandler.typeUseLockHandler<T>()
quartz.scheduler.jobFactory.typeUseJobFactory<T>()
quartz.scheduler.typeLoadHelper.typeUseTypeLoader<T>()
quartz.jobListener.NAME.typeAddJobListener<T>(matchers)
quartz.triggerListener.NAME.typeAddTriggerListener<T>(matchers)

A listener named by properties has no matchers to carry, so it listens to everything. The code-first methods take matchers, which is the reason to prefer them.

Every key has both spellings. quartz.jobStore.tablePrefix and JobStore:TablePrefix are the same setting said two ways, and so are the ones that select an implementation rather than set a value — JobStore:Type, JobStore:DriverDelegateType, JobStore:LockHandler:Type, ThreadPool:Type and the rest. A configuration file never has to mix the two forms, and a component with no options type of its own is still configurable, because its settings are read as flat keys whichever way they were written.

Durations may be written either way too: 00:00:30 or a bare 30000, which is read as milliseconds for the sake of configuration files carried forward from 3.x.

Where the same setting is said twice, code wins. A UsePersistentStore in code beats a leftover quartz.jobStore.type in a configuration file, and a value set through ConfigureScheduler beats the same value in appsettings.json. Built-in fallbacks — the driver delegate and the serializer — are registered after everything explicit, so they only apply when nothing else claimed the slot.

Removed in 4.x, with no replacement: quartz.scheduler.proxy* and quartz.scheduler.exporter* (remoting, which .NET no longer supports) — these two are rejected with an exception naming the replacement, rather than accepted and ignored — plus quartz.threadExecutor*, which had no implementation left to choose between.

Help us by improving this page!
Last Updated: 8/19/26, 7:27 PM
Contributors: Marko Lahma, Claude Fable 5
Prev
Tutorial
Next
JSON Configuration