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

Quartz.NET Configuration Reference

  • Scheduler
  • Thread pool
  • In-memory job store
  • Persistent job store
    • Databases
    • Locking
    • Data source
    • Clustering
  • Serialization
  • Scheduling
  • Job factory
  • Type loader
  • The other seams
  • Health check
  • Execution history
  • Listeners, calendars and plugins
  • Several schedulers
  • Without a container
  • Legacy property keys
    • Unknown job store keys

Options are strongly typed, and each has the same name in code and in a configuration file:

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

Options are bound from the Quartz section by section name and validated at startup, so a bad value is reported against the setting that is wrong. The binding is source-generated, so configuring from a file is as safe under PublishTrimmed and PublishAot as configuring in code, with nothing asked of your application.

SectionOptions
SchedulerQuartzSchedulerOptionsbelow
ThreadPoolThreadPoolOptionsbelow
JobStoreInMemoryJobStoreOptions or AdoJobStoreOptionsbelow
JobStore:ClusteringClusteringOptionsbelow
DataSourceDataSourceOptions, one per named data sourcebelow
SchedulingSchedulingOptions: when registered jobs and triggers already exist in the storebelow
TypeLoaderTypeLoaderOptions, the container's rather than a scheduler'sbelow
Schedulersone sub-section per named schedulerbelow
Schedule, ProcessingDirectivesjobs and triggers declared in configurationJSON configuration

Tips

Everything on this page can also be written as the flat quartz.* keys earlier versions used; Quartz still accepts them. See Legacy property keys.

Scheduler

QuartzSchedulerOptions, bound from Quartz:Scheduler.

OptionTypeDefaultDescription
InstanceNamestringQuartzSchedulerDistinguishes schedulers in one process. Every node in a cluster must share it.
InstanceIdstringNON_CLUSTEREDMust be unique among the nodes of a cluster.
GenerateInstanceIdboolfalseDerives InstanceId at startup from the registered IInstanceIdGenerator.
IdleWaitTimeTimeSpan00:00:30How long to wait before re-querying the job store when nothing is due. At least one second.
MaxBatchSizeint0: the pool on a persistent store, 1 in memoryUpper bound on triggers acquired at once; may not exceed ThreadPool:MaxConcurrency. See Batching trigger acquisition.
BatchTriggerAcquisitionFireAheadTimeWindowTimeSpan00:00:00How far ahead of its fire time a trigger may join the current batch. At zero, a batch takes the triggers already due.
ShutdownJobInterruptionShutdownJobInterruptionNeverWhen a shutting-down scheduler signals cancellation to running jobs.
PropagateTraceContextbooltrueStores the ambient trace context on a trigger scheduled inside an Activity, so the firing's span links back. See below.
ContextdictionaryemptyValues seeded into SchedulerContext. Get-only: add to it (options.Context["environment"] = "staging").
  • MaxBatchSize at 0 resolves when the scheduler is built: ThreadPool:MaxConcurrency on a persistent store, clustered or not, and 1 in memory. An explicit value wins; 1 is 4.2's behaviour.
  • MaxBatchSize is only an upper bound. A batch ends at the first trigger's fire time, or now if that is later, plus BatchTriggerAcquisitionFireAheadTimeWindow.
  • PropagateTraceContext writes two reserved job-data keys. They are visible wherever trigger data is: MergedJobDataMap, the dashboard, GET /triggers, QRTZ_TRIGGERS.JOB_DATA. Turn it off to keep them out of the store. See OpenTelemetry integration.

ShutdownJobInterruption values:

ValueMeaning
NeverRunning jobs are never interrupted.
WhenNotWaitingForJobsInterrupted only on a shutdown that does not wait for them.
WhenWaitingForJobsInterrupted only on a shutdown that waits. The wait still happens, so a job that checks its cancellation token can unwind cleanly.
AlwaysInterrupted on every shutdown.
services.AddQuartz(q => q.ConfigureScheduler(options =>
{
    options.InstanceName = "core";
    options.InstanceId = "node-1";
    options.MaxBatchSize = 5;
    options.ShutdownJobInterruption = ShutdownJobInterruption.Always;
}));

Thread pool

ThreadPoolOptions, bound from Quartz:ThreadPool.

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

Your own implementation:

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

Two shipped pools are reached through UseThreadPool<T>():

  • Quartz.Impl.ZeroSizeThreadPool — for a scheduler that only writes the schedule and is never started. It creates no threads, and the two members a running scheduler calls throw NotSupportedException, so Start() fails loudly. Such a process needs no reference to the job classes' assemblies and no ITypeLoader of its own: the persistent store edits the schedule by stored type name alone.
  • Quartz.Impl.TaskSchedulingThreadPool — the open base of DefaultThreadPool. Derive from it and override one member, GetDefaultScheduler(), instead of implementing all six of IThreadPool's.

ThreadPoolOptions belongs to the built-in pools (they read MaxConcurrency), so UseThreadPool<T>() takes no callback for it. Your own pool has its own options:

services.AddQuartz(q =>
{
    q.ConfigureOptions<MyThreadPoolOptions>(options => options.Slots = 20);
    q.UseThreadPool<MyThreadPool>();
});

ConfigureOptions<TOptions> registers the callback under this scheduler's options name and declares the type as the scheduler's own. A component that takes IOptions<MyThreadPoolOptions> then gets the values configured for its scheduler, not the unnamed instance. It works for any container-built component — a job store, a lock handler, a listener, a job factory — and AddPlugin<T, TOptions>() builds on it.

In-memory job store

InMemoryJobStoreOptions, bound from Quartz:JobStore. The default store; it does not survive a process restart.

OptionTypeDefaultDescription
MisfireThresholdTimeSpan00:00:05How late a trigger may fire before it counts as misfired.
MaxConsecutiveFireFailuresint5Failed fires in a row after which a trigger is set ERROR. 0: never. See A trigger that fails to fire.
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 for its SQL dialect, so a connection string is usually all you supply:

services.AddQuartz(q => q.UsePersistentStore(store =>
{
    store.UseSqlServer(connectionString);
}));
OptionTypeDefaultDescription
TablePrefixstringQRTZ_Prefix on every Quartz table name.
StoreJobDataAsStringsboolfalsePersists job data as name/value strings, not serialized objects, so it stays readable and version tolerant.
MisfireThresholdTimeSpan00:01:00How late a trigger may fire before it counts as misfired.
MisfireHandlerFrequencyTimeSpan?MisfireThresholdHow often misfires are handled.
MaxMisfiresToHandleAtATimeint20Misfired triggers handled per pass.
CommandTimeoutTimeSpan?provider defaultTimeout for every statement the store issues, the lock handler's included. See below.
LockWaitWarningThresholdTimeSpan?00:00:30How long one attempt to take a job store lock may wait before warning 3716 is logged. See below.
DbRetryIntervalTimeSpan00:00:15Back-off after a database failure, for the misfire loop and the check-in loop. See below.
MaxTransientRetriesint3How many times a transient failure, such as a deadlock, is retried. See below.
TransientRetryIntervalTimeSpan00:00:01Delay between transient retries.
RetryableActionErrorLogThresholdint4Consecutive failures before they are logged as errors.
IsTransientFunc<Exception, bool>?nullExtra transient test for a driver the built-in list misses. Code only. See below.
MaxConsecutiveFireFailuresint5Failed fires in a row after which a trigger is stored ERROR. 0: never. See below.
UseDbLocksboolfalseUses database row locks. Required for clustering, and implied by UseClustering().
LockOnInsertbooltrueTakes a lock when inserting rows.
AcquireTriggersWithinLockboolfalseAcquires triggers inside the database lock.
TransactionIsolationLevelIsolationLevel?noneIsolation level of the store's own transactions. Unset: ReadCommitted. See below.
AcceptEnlistedTransactionsboolfalseLets the store use a connection the application enlisted with SchedulerEnlistmentExtensions.EnlistTransaction, so scheduling commits with the application's work. See Joining an existing transaction.
DoubleCheckLockMisfireHandlerbooltrueRe-checks the lock before handling misfires.
UseBackgroundThreadsboolfalseRuns the misfire handler and cluster manager on background threads, which do not keep the process alive. They are the only real threads Quartz creates.
SchemaProvisioningSchemaProvisioningValidateAt startup: None does nothing, Validate checks the expected tables exist, CreateIfMissing creates what is missing and then checks.
SelectWithLockSqlstring?noneOverrides the row-lock statement; defaults to SQL Server's WITH (UPDLOCK,ROWLOCK) form on SQL Server. See Locking.
OpenConnectionboolfalseWhether the ambient-transaction store opens the connections it creates. See below.
  • CommandTimeout: unset leaves each provider's default, usually 30 seconds. ADO.NET counts whole seconds, so the value is rounded up — 00:00:01.500 becomes 2 seconds — because rounding down would turn a sub-second value into 0, which means "no timeout".
  • LockWaitWarningThreshold: logged once per acquisition, naming the lock and the wait so far. It is the only report of a node stalled on a lock, since a blocked statement neither returns nor throws. It does not end the wait; CommandTimeout or a wait timeout in the lock statement does. null turns it off; zero is refused. See A Lock Held by a Connection That Is Gone.
  • DbRetryInterval: the check-in loop waits this long once a failed check-in has spent the window its peers give it (CheckinInterval + CheckinMisfireThreshold). Inside that window it retries sooner, and this only caps the wait.
  • MaxTransientRetries: transient means the driver's own DbException.IsTransient; a SQLSTATE in class 40 ("transaction rollback": a serialization failure or deadlock, whichever provider reports it), except 40002, a deferred constraint violation that fails the same way every time; SQL Server's transient error numbers; SQLite's busy and locked codes; or a timeout.
  • IsTransient: consulted first and only additive — false falls through to the built-in list, so it cannot stop a retry Quartz already makes. It receives the store's own exception; reach the driver's with GetBaseException(). There is no quartz.* key for a delegate.
  • MaxConsecutiveFireFailures: counts fires that fail for a reason a retry will not cure, such as a constraint violation. The trigger is stored ERROR, listeners hear TriggerInError, and event 3050 is logged. See A trigger that fails to fire.
    • Not counted: a transient failure, a failure of the whole batch, a failed acquisition. A database outage fails acquisition first, so it parks nothing.
    • A fire that commits, on any node, starts the count again.
    • Each node counts alone, so a cluster may try a trigger this many times per node.
    • A failure that is really transient belongs in IsTransient, which retries it instead.
  • TransactionIsolationLevel: the ReadCommitted default is Quartz's, not the provider's (providers vary). Forced to Serializable on SQLite. Ignored for a connection the application enlisted, which runs at the application's level.
  • SelectWithLockSql is read only when the store builds a database-locking handler for itself.
  • OpenConnection is read only by ExternalTransactionJobStore, the store UsePersistentStore(store => store.UseAmbientTransactions()) selects. It is written by quartz.jobStore.openConnection as well as by the section entry.

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

Databases

MethodDatabaseDriver package your project references
UseSqlServerMicrosoft SQL ServerMicrosoft.Data.SqlClient
UsePostgresPostgreSQLNpgsql
UseMySqlMySQL, using the MySql.Data driverMySql.Data
UseMySqlConnectorMySQL, using the MySqlConnector driverMySqlConnector
UseOracleOracleOracle.ManagedDataAccess.Core
UseFirebirdFirebirdFirebirdSql.Data.FirebirdClient
UseSqliteSQLite, using the Microsoft.Data.Sqlite driverMicrosoft.Data.Sqlite
UseSystemDataSqliteSQLite, using the legacy System.Data.SQLite driverSystem.Data.SQLite.Core
UseGenericDatabaseAnything else, with the generic SQL dialect; the only one that can describe its own driverthe one your driver ships in

Quartz references none of these packages; the application adds its own. A method called without its driver present compiles, then fails at startup with Could not load file or assembly. See Job Stores.

Each takes a connection string or a callback over DataSourceOptions:

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

To connect through a DbDataSource registered in the container instead, set store.UseSqlServer(db => db.UseRegisteredDataSource = true).

Naming a driver, or handing over its factory

Both spellings above name the driver. SqlServer names a description of which connection, command and parameter types to instantiate, and Quartz resolves those types from strings, since it references no driver package. Most applications want this.

A trimmed or native AOT application cannot rely on it: the trimmer does not follow a type name and removes what it pointed at, and the registration fails while the container is built with Cannot instantiate type which has no empty constructor. Every method above also takes the driver's DbProviderFactory:

store.UseSqlServer(SqlClientFactory.Instance, connectionString);
store.UsePostgres(NpgsqlFactory.Instance, connectionString);
store.UseMySqlConnector(MySqlConnectorFactory.Instance, connectionString);
store.UseSqlite(SqliteFactory.Instance, connectionString);
store.UseSystemDataSqlite(SQLiteFactory.Instance, connectionString);
store.UseFirebird(FirebirdClientFactory.Instance, connectionString);
store.UseMySql(MySqlClientFactory.Instance, connectionString);

The factory creates the connection, the connection the command, and the command its parameters, so nothing is named or built by reflection. The provider name is still chosen for you, since it decides how the driver spells a parameter, but only the part of its description that names no type is read.

RegistrationWhen
Use<Db>(connectionString)The ordinary case. Carries [RequiresUnreferencedCode], so a trimmed publish reports it.
Use<Db>(factory, connectionString)PublishTrimmed or PublishAot, or anywhere you do not want a type resolved from a string.
db.UseRegisteredDataSource = trueA DbDataSource in the container already holds the connection details — pooling, type mappers, logging. Also free of type names.

Oracle needs more than a factory, because Quartz reaches three things on its types by reflection and a factory names none of them. Say them in code:

store.UseOracle(
    OracleClientFactory.Instance,
    connectionString,
    configureCommand: command => ((OracleCommand) command).BindByName = true,
    configureBinaryParameter: parameter => ((OracleParameter) parameter).OracleDbType = OracleDbType.Blob,
    configureLargeTextParameter: parameter => ((OracleParameter) parameter).OracleDbType = OracleDbType.Clob);
  • Without configureCommand, ODP.NET binds parameters by position and the store reads the wrong columns.
  • Without configureBinaryParameter, a job data map over two kilobytes will not fit, because that driver maps DbType.Binary to OracleDbType.Raw, not Blob.
  • Without configureLargeTextParameter, a captured log over 4,000 bytes fails its history row with ORA-01461, because a string is bound as Varchar2. It matters only with UseExecutionHistory() and UseExecutionLogCapture().

Naming the driver instead of passing its factory sets all three for you.

The factory overloads take the connection string directly, so ConnectionStringName does not apply; read the connection string from IConfiguration and pass it in.

PostgreSQL: DISCARD ALL on every connection return

Npgsql resets a pooled connection on return by sending DISCARD ALL: one round trip per return, recorded by the server as a transaction of its own, so PostgreSQL counts about twice the transactions Quartz opens. At the shipped defaults a firing borrows three connections (acquisition, fire and completion are separate transactions), so it costs three DISCARD ALL round trips and three extra commits — most of a pg_stat_database reading of a Quartz deployment.

No Reset On Close=true on the connection string turns it off:

Host=db;Database=quartznet;Username=quartz;Password=…;No Reset On Close=true

Measured on a drain of 500 one-off firings against PostgreSQL 15.1 with fsync=on, pool size 10 (dotnet run -c Release --project src/Quartz.Benchmark -- --one-off-census):

commits per firingstatements per firing
shipped defaults6.0123.0
with No Reset On Close=true3.0120.0

It is a connection-string decision, and Quartz does not change it for you.

  • Over loopback, firings per second did not move beyond run-to-run variance. Across a network, or behind a managed database's connection proxy, measure what three fewer round trips per firing buy.
  • A connection returns to the pool carrying its session state: SET statements, prepared statements, listen/notify registrations, temporary tables. Quartz sets none, so it is safe for a data source only Quartz uses — not for one shared with application code that sets session state.

Describing a driver Quartz does not know

A provider name (SqlServer, Npgsql, …) 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 every database 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",
});

A four-argument overload takes a DataSourceOptions callback instead of a connection string, for a described driver with a named connection string.

A driver reached through its own factory is described the same way and needs no provider name:

store.UseGenericDatabase(MyFactory.Instance, connectionString, new DbMetadata
{
    ProductName = "My Database",
    ParameterNamePrefix = "@",
    UseParameterNamePrefixInParameterCollection = true,
    BindByName = true,
    ConfigureBinaryParameter = parameter => ((MyParameter) parameter).MyDbType = MyDbType.Blob,
});
  • ConfigureCommand and ConfigureBinaryParameter are the two typed seams on DbMetadata. They say what the name path reaches by reflecting over CommandType and ParameterType, so a description that names no type is still complete. Either may be unset: a binary parameter with neither a seam nor a described parameter type is bound as DbType.Binary, which every driver that ships a factory maps itself.
  • DbLargeTextTypeName and ConfigureLargeTextParameter say the same for the execution history's captured log, for a driver whose string parameter cannot reach a large text column (Oracle's Clob). Unset, the log is bound as a string, which every other shipped driver takes.
  • A description is a container registration, not process-wide state, so two containers in one process need not agree on a provider name. Within one container a name means one thing; two schedulers needing two drivers use two names.
  • Describing a name Quartz already ships replaces it. A description registered in code wins over quartz.dbprovider.* keys. Several drivers mean several calls, one per name.

The same description as properties, the 3.x form, now read through IConfiguration:

{
  "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"
  }
}

Locking

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

  • SelectWithLockSql belongs to the handler the store builds for itself. A handler from UseLockHandler takes its statement through its own constructor, so setting both leaves the option unused, and the store logs a warning at startup.
  • SelectWithLockSql is also where a server-side lock wait timeout goes — on Oracle, the only place. A statement ending FOR UPDATE WAIT 20 fails with ORA-30006 instead of waiting behind a lock nobody will release — see A Lock Held by a Connection That Is Gone.
  • This and UseSerializer register against the scheduler that owns the store. ILockHandler or IObjectSerializer registered directly on Services is registered for the container, and a named scheduler does not see it.
  • The store tells any handler which scheduler it locks for, and its clock and CommandTimeout, through ILockHandler.Initialize(LockHandlerContext) before the first lock; a handler of your own needs no configuring for them.
  • Set the timeout on a clustered store: a node waiting on QRTZ_LOCKS behind a peer that stopped without releasing the row schedules nothing until the lock statement gives up.

Data source

DataSourceOptions, bound from Quartz:DataSource.

OptionTypeDescription
ProviderstringThe driver description to use. Set by the database methods; see the constants below, and Describing a driver Quartz does not know.
ConnectionStringstring?The connection string. Takes precedence over ConnectionStringName.
ConnectionStringNamestring?A connection string to resolve from IConfiguration.
UseRegisteredDataSourceboolConnections come from the container's unkeyed DbDataSource. Wins over both connection string settings.
DataSourceServiceKeyobject?The service key of the DbDataSource, for a container holding several. Implies UseRegisteredDataSource. Code only.
DataSourceFactoryFunc<IServiceProvider, DbDataSource>?Supplies the DbDataSource directly. Wins over both of the above. Code only.

The provider names Quartz ships a description for, as constants on DataSourceOptions.Providers:

ConstantValueDriver
DataSourceOptions.Providers.SqlServerSqlServerMicrosoft.Data.SqlClient
DataSourceOptions.Providers.NpgsqlNpgsqlNpgsql
DataSourceOptions.Providers.MySqlMySqlMySql.Data
DataSourceOptions.Providers.MySqlConnectorMySqlConnectorMySqlConnector
DataSourceOptions.Providers.OracleOracleODPManagedmanaged ODP.NET
DataSourceOptions.Providers.SqliteSQLite-MicrosoftMicrosoft.Data.Sqlite
DataSourceOptions.Providers.SystemDataSqliteSQLiteSystem.Data.SQLite
DataSourceOptions.Providers.FirebirdFirebirdFirebirdSql.Data.FirebirdClient

Provider is a string, not an enum, because the set is open: UseGenericDatabase describes drivers Quartz does not know.

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);
}));

UseRegisteredDataSource asks for the container's one unkeyed DbDataSource, which fits an application with one database. A container with several (a scheduler per tenant, or a reporting scheduler beside the application's) keys them, and DataSourceServiceKey names this store's key:

services.AddNpgsqlDataSource(tenantA, serviceKey: "tenant-a");
services.AddNpgsqlDataSource(tenantB, serviceKey: "tenant-b");

services.AddQuartz("tenant-a", q => q.UsePersistentStore(store =>
    store.UsePostgres(db => db.DataSourceServiceKey = "tenant-a")));
services.AddQuartz("tenant-b", q => q.UsePersistentStore(store =>
    store.UsePostgres(db => db.DataSourceServiceKey = "tenant-b")));

A data source you build rather than register goes in DataSourceFactory, which wins over both:

store.UsePostgres(db => db.DataSourceFactory = _ => BuildDataSource());
  • Both are set in code: a service key can be any object and a factory is a delegate, neither of which a configuration binder can produce. With either, Quartz needs no connection string.
  • On this path the connection makes the commands, not the driver description, so whatever the data source configured — an NpgsqlDataSource's type mappers, logging, composite type registrations — applies to Quartz's statements too.

A store's data source is named after the scheduler that owns it, or quartz for the default scheduler. To name it explicitly, call store.UseDataSource("reporting-db") before choosing the database (the name is fixed once the data source is configured) — when two stores should read the same Quartz:DataSource:<name> settings, or the settings live under a name of your choosing.

The three data-source entry points:

  • UseDataSource(configure) defines one: which driver, and how to reach the database. The database methods are shorthands for it.
  • UseDataSource(name) refers to one by name, to pick up settings registered elsewhere, such as a Quartz:DataSource:<name> section.
  • Where the connection comes from is set on DataSourceOptions, not by another method.

Bringing your own connection provider

When connections cannot be described — a pooled or credential-rotating factory, or a driver whose connections need setting up after creation — hand Quartz the object that makes them:

services.AddQuartz(q => q.UsePersistentStore(store =>
{
    store.UseSqlServer(connectionString);          // still selects the driver delegate
    store.UseConnectionProvider<MyDbProvider>();   // …but connections come from here
}));
  • UseConnectionProvider(factory) does the same for a provider that needs building.
  • It is the one builder method that replaces rather than defers: it wins over the database method's provider in either order.
  • It also names the store's data source, so it is a complete configuration on its own; the database method only selects the driver delegate.
  • The provider belongs to the scheduler that owns the store. IDbProvider registered on Services is the container's, and a named scheduler does not see it.

As properties, the 3.x spelling, still read:

{
  "Quartz": {
    "quartz.jobStore.dataSource": "myDs",
    "quartz.dataSource.myDs.connectionProvider.type": "MyNamespace.MyDbProvider, MyAssembly"
  }
}

Clustering

Several schedulers share one database, and a node's triggers are recovered by another if it dies. Every node uses the same InstanceName and a different InstanceId.

ClusteringOptions, bound from Quartz:JobStore:Clustering — the only place clustering is configured.

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. The node's own failed check-in is retried inside it, so a shorter database blip does not get the node written off.
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);
        });
    });
});

UseClustering() also enables database locking; clustering does not work without it.

Serialization

What a persistent store cannot write as a string goes through an IObjectSerializer. A store that names none gets SystemTextJsonObjectSerializer, registered as a fallback like the driver delegate, so UseSystemTextJsonSerializer() with no argument changes nothing. Two calls do change something:

// Newtonsoft.Json, from the Quartz.Serialization.Newtonsoft package: what reads data
// a 3.x scheduler's Newtonsoft serializer wrote
store.UseNewtonsoftJsonSerializer();

// System.Text.Json with something to say about it — a trigger or calendar type of
// your own that the built-in serializers do not know
store.UseSystemTextJsonSerializer(json =>
    json.AddTriggerSerializer<CustomTrigger>(new CustomTriggerSerializer()));

Scheduling

SchedulingOptions, bound from Quartz:Scheduling: what happens when jobs and triggers registered in code, or declared in a file, already exist in the store under the same names.

OptionTypeDefaultDescription
OverwriteExistingDatabooltrueA registration replaces the stored job or trigger of the same name. Setting IgnoreDuplicates turns this default off.
IgnoreDuplicatesboolfalseAn existing name is skipped instead of throwing. Turns OverwriteExistingData off; setting both explicitly is refused at startup.
ScheduleTriggerRelativeToReplacedTriggerboolfalseA replaced trigger's next fire time is computed from the old trigger's last fire time, not from now.

These compare a file or registration with the store. None of them covers one scheduling data file that declares the same job or trigger key twice, which is always an error — see ProcessingDirectives.

services.Configure<QuartzOptions>(options => options.Scheduling.IgnoreDuplicates = true);

Job factory

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

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

To keep it and add to the scope it opens:

services.AddQuartz(q => q.ConfigureJobScope((scope, bundle, scheduler) => { /* … */ }));

Type loader

TypeLoaderOptions, bound from Quartz:TypeLoader. The one options type that is the container's, because one ITypeLoader serves every scheduler in the container.

OptionTypeDefaultDescription
AliasesDictionary<string, string>emptyMaps a type name that no longer exists, as stored or configured, to the type that replaced it.
services.AddQuartz(q => q.UseTypeLoader(loader =>
    loader.Map("Acme.Jobs.NightlyReport, Acme.Jobs", typeof(NightlyRollupJob))));
{
  "Quartz": {
    "TypeLoader": {
      "Aliases": {
        "Acme.Jobs.NightlyReport, Acme.Jobs": "Acme.Jobs.NightlyRollupJob, Acme.Jobs"
      }
    }
  }
}
  • An alias applies wherever Quartz turns a string into a type at run time: a stored JOB_CLASS_NAME, a job named in XML or JSON scheduling data, a quartz.plugin.<name>.type key.
  • Flat keys naming a scheduler's own components are read while the service collection is built, before options exist, and are not aliased.
  • An alias whose target names no loadable type fails options validation at startup.
  • Nothing is written back, so retiring an alias still needs the SQL UPDATE — see Job deserialization failures after refactoring.

The other seams

Each replaces one collaborator of the scheduler. All are IQuartzBuilder members, so they work the same under AddQuartz and QuartzSchedulerBuilder.Create(q => …).

MethodReplacesDefault
UseTimeProvider(timeProvider)the clock every trigger, store and misfire calculation readsTimeProvider.System, or the container's registration if any
UseTypeLoader<T>()how a type named by a string (a stored JOB_CLASS_NAME, a .type key) is resolvedresolution through the container's assemblies, with the 3.x namespace fallbacks
UseTypeLoader(configure)configures the loader instead: loader.Map(oldName, typeof(NewType)), the same map as Quartz:TypeLoader:Aliasesno aliases
UseSimpleTypeLoader()selects the built-in loader, which is internal, so there is no UseTypeLoader<SimpleTypeLoader>(). Matters only if something else registered a loader firstthis is the default
UseInstanceIdGenerator<T>()how InstanceId is derived when GenerateInstanceId is onSimpleInstanceIdGenerator: host name plus a timestamp
UseJobStore<T>(), UseJobStore<T, TOptions>()the job store, for one Quartz does not shipthe in-memory store
UseDriverDelegate<T>(), UseDriverDelegate(factory) (persistent store builder)the SQL dialect of the ADO.NET storechosen by the database method: UseSqlServer picks SqlServerDelegate, and so on

UseTimeProvider is for tests: a FakeTimeProvider makes TriggerBuilder, GetFireTimeAfter and misfire calculations see the time you set, and advancing it wakes the scheduler's own waits.

Health check

QuartzHealthCheckOptions, set by AddHealthChecks().AddQuartz(configure) or the scheduler's own AddQuartzHealthChecks(configure). Options are per scheduler name.

OptionTypeDefaultDescription
Namestring?quartz-scheduler, or quartz-scheduler-<scheduler name>The check's registered name.
TagsList<string>emptyWhat a probe filters on. Add to it; a blank or repeated tag is refused at startup.
FailureStatusHealthStatus?UnhealthyWhat a failed check reports.
StandbyStatusHealthStatus?DegradedWhat a scheduler in standby reports. Standby only: a scheduler waiting for the application to start it stays degraded.
ClusterCheckinTolerancedouble?3How many of its own check-in intervals a clustered node may miss before the check reports degraded. null or 0: no query. Unclustered schedulers read nothing.
StaleFiringTolerancedouble?nullHow many of the store's misfire thresholds a schedulable trigger may be overdue before degraded; twice that is unhealthy. null or 0: no query. 3 is a starting value.
RequiredJobsList<RequiredJobOptions>emptyJobs that must have succeeded within a window. Add with RequireSuccessWithin(job, window, status). See Alert when a job stops succeeding.

StaleFiringTolerance is off by default because what counts as overdue is the application's call. Standby and paused schedulers report as usual with it on. See Health checks and probes for what each verdict means to a probe and what the check does not assert.

Execution history

ExecutionHistoryOptions, set by AddQuartzExecutionHistory(configure). One set for the container.

OptionTypeDefaultDescription
RetentionTimeSpan1.00:00:00Age of every result RetentionByResult does not name.
RetentionByResultDictionary<JobRunResult, TimeSpan>emptyAge per result, matched on EffectiveResult. Add to it.
MisfireRetentionTimeSpan?null: RetentionAge of the misfire feed.
MaxEntriesPerJobint0: no capRows kept per job, earliest-fired out first. Failed rows are exempt.
MaxEntriesPerSchedulerint2000Rows kept per scheduler in each feed, oldest out first. 0 records nothing.

A zero or negative age, or a negative count, fails at startup. The database history applies all five by a sweep, from one node per cluster; see Execution history in the database and Keep history by result.

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 and take dependencies through their constructors.

Several schedulers

A scheduler registered under a name has its own job store, thread pool, jobs and configuration. The name is its 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 without a host use QuartzSchedulerBuilder. Create hands the callback the same IQuartzBuilder that AddQuartz(q => …) does, over a container it creates itself:

IScheduler scheduler = await QuartzSchedulerBuilder
    .Create(q => q
        .ConfigureScheduler(options => options.InstanceName = "reporting")
        .UseDefaultThreadPool(maxConcurrency: 20)
        .UseInMemoryStore())
    .BuildScheduler();

It adds only the terminal methods: Build() for the factory and BuildScheduler() for the scheduler. AddJob, AddTrigger, ScheduleJob, AddCalendar and every package's extensions go inside the callback, as under a host.

A scheduler configured entirely by flat quartz.* keys:

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

UseProperties checks keys against the ones Quartz reads and reports a misspelling; set quartz.checkConfiguration to false to allow keys of your own. Code wins over properties, whichever is applied first.

The check covers a property bag you wrote, not appsettings.json.

  • It runs for UseProperties and the AddQuartz(services, properties, …) overloads, the shape a 3.x application migrates in.
  • Keys from an IConfiguration section are not checked: every key under Quartz: becomes a quartz.* key whether Quartz reads it or not, so your own settings there would be rejected.
  • So a misspelled key in appsettings.json is silently ignored: Quartz:Scheduler:IdelWaitTime leaves the default thirty seconds in force. Check a new key against the tables above.
  • Casing is not the risk: configuration keys are case-insensitive, so Quartz:Jobstore:TablePrefix is Quartz:JobStore:TablePrefix.

The job store is checked either way. A persistent store refuses a quartz.jobStore.* key that nothing reads, written flat or as Quartz:JobStore:TabelPrefix — see Unknown job store keys. The keys under that prefix are known exhaustively: the store's options, its clustering sub-section, its lock handler's keys, and the two keys that select a type.

Legacy property keys

Earlier versions used flat quartz.* string keys. They still work: each is translated into the option above and produces the same result.

  • Flat-format durations are integer milliseconds (quartz.scheduler.idleWaitTime = 30000); typed options are TimeSpan ("00:00:30").
  • A .type key names an implementation. In code, use the matching Use* method, which the compiler checks.
Flat keyOption
quartz.scheduler.instanceNameScheduler:InstanceName
quartz.scheduler.instanceIdScheduler:InstanceId (AUTO and SYS_PROP set GenerateInstanceId)
quartz.scheduler.idleWaitTimeScheduler:IdleWaitTime
quartz.scheduler.batchTriggerAcquisitionMaxCountScheduler:MaxBatchSize
quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindowScheduler:BatchTriggerAcquisitionFireAheadTimeWindow
quartz.scheduler.interruptJobsOnShutdownScheduler:ShutdownJobInterruption — true alone means WhenNotWaitingForJobs
quartz.scheduler.interruptJobsOnShutdownWithWaitScheduler:ShutdownJobInterruption — true alone means WhenWaitingForJobs; both keys true means Always
quartz.context.key.NAMEScheduler:Context:NAME
quartz.threadPool.maxConcurrency (or threadCount)ThreadPool:MaxConcurrency
quartz.threadPool.typeUseThreadPool<T>()
quartz.jobStore.typeUseInMemoryStore() / UsePersistentStore(), with UseAmbientTransactions() inside it for the store 3.x called JobStoreCMT; UsePersistentStore<T>() for a persistent store of your own
quartz.jobStore.misfireThresholdJobStore:MisfireThreshold
quartz.jobStore.tablePrefixJobStore:TablePrefix
quartz.jobStore.usePropertiesJobStore:StoreJobDataAsStrings
quartz.jobStore.makeThreadsDaemonsJobStore:UseBackgroundThreads
quartz.jobStore.clusteredJobStore:Clustering:Enabled, or UseClustering()
quartz.jobStore.acceptEnlistedTransactionsJobStore:AcceptEnlistedTransactions
quartz.jobStore.openConnectionJobStore:OpenConnection, read only by the ambient-transaction store
quartz.jobStore.clusterCheckinIntervalJobStore:Clustering:CheckinInterval
quartz.jobStore.clusterCheckinMisfireThresholdJobStore:Clustering:CheckinMisfireThreshold
quartz.jobStore.clustering.enabledJobStore:Clustering:Enabled — the hierarchical spelling of quartz.jobStore.clustered
quartz.jobStore.clustering.checkinIntervalJobStore:Clustering:CheckinInterval
quartz.jobStore.clustering.checkinMisfireThresholdJobStore:Clustering:CheckinMisfireThreshold
quartz.jobStore.driverDelegateTypeJobStore:DriverDelegateType; the UseSqlServer() family sets it
quartz.jobStore.schemaProvisioningJobStore:SchemaProvisioning — None, Validate or Create
quartz.jobStore.performSchemaValidationJobStore:SchemaProvisioning — true means Validate, false means None; the key above says all three
quartz.jobStore.executionHistoryUseExecutionHistory() — keeps the execution history in this scheduler's database. Needs the history's tables and columns, from database/migrations/4.2/add_execution_history_<db>.sql onwards; see Execution history in the database
quartz.jobStore.useDBLocksJobStore:UseDbLocks
quartz.jobStore.lockOnInsertJobStore:LockOnInsert
quartz.jobStore.acquireTriggersWithinLockJobStore:AcquireTriggersWithinLock
quartz.jobStore.selectWithLockSQLJobStore:SelectWithLockSql
quartz.jobStore.txIsolationLevelSerializableJobStore:TransactionIsolationLevel — true means Serializable; unset says nothing, not ReadCommitted
quartz.jobStore.misfireHandlerFrequencyJobStore:MisfireHandlerFrequency
quartz.jobStore.maxMisfiresToHandleAtATimeJobStore:MaxMisfiresToHandleAtATime
quartz.jobStore.doubleCheckLockMisfireHandlerJobStore:DoubleCheckLockMisfireHandler
quartz.jobStore.maxTransientRetriesJobStore:MaxTransientRetries
quartz.jobStore.transientRetryIntervalJobStore:TransientRetryInterval
quartz.jobStore.dbRetryIntervalJobStore:DbRetryInterval
quartz.jobStore.commandTimeoutJobStore:CommandTimeout — added in 3.22, in milliseconds; 0 means the provider's default, i.e. the option left unset
quartz.jobStore.retryableActionErrorLogThresholdJobStore:RetryableActionErrorLogThreshold
quartz.jobStore.maxConsecutiveFireFailuresJobStore:MaxConsecutiveFireFailures
quartz.jobStore.dataSourceset 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.serializer.PROPERTYsets that property on the serializer — e.g. quartz.serializer.RegisterTriggerConverters = true
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.scheduler.instanceIdGenerator.typeUseInstanceIdGenerator<T>(); other quartz.scheduler.instanceIdGenerator.* keys configure it
quartz.timeProvider.typeUseTimeProvider(timeProvider)

Nine key prefixes are rejected rather than ignored, because they no longer configure anything. Each is reported by name with its replacement, not as an unknown property, since a configuration carrying one was configuring something real:

Rejected keyWhyInstead
quartz.scheduler.threadName, quartz.scheduler.makeSchedulerThreadDaemonthe scheduling loop is a Task, not a Thread: it has no name and never held a process openremove them; for the misfire and cluster threads, which are real threads, use quartz.jobStore.makeThreadsDaemons / JobStore:UseBackgroundThreads
quartz.jobListener.NAME.type, quartz.triggerListener.NAME.typea listener named by properties had no matchers, so it heard everything, and its type was found by reflectionAddJobListener<T>(matchers) and AddTriggerListener<T>(matchers), which take the matchers and build the listener through the container
quartz.jobStore.lockHandler.tablePrefix, quartz.jobStore.lockHandler.schedName, quartz.jobStore.lockHandler.schedulerNamethe job store tells the lock handler its table prefix and scheduler name through ILockHandler.Initializeset quartz.jobStore.tablePrefix and quartz.scheduler.instanceName; a 3.x configuration that sets these fails at startup
quartz.scheduler.proxy*, quartz.scheduler.exporter*remoting, which .NET no longer supportsnothing

Removed in 4.x, with no replacement: quartz.scheduler.proxy* and quartz.scheduler.exporter* (rejected with an exception, as above, not ignored) and quartz.threadExecutor* (no implementation left to choose).

  • Every key has both spellings, including those that select an implementation: JobStore:Type, JobStore:DriverDelegateType, JobStore:LockHandler:Type, ThreadPool:Type and the rest. A file never has to mix the forms, and a component with no options type of its own is still configurable, since its settings are read as flat keys either way.
  • Durations may be 00:00:30 or a bare 30000, read as milliseconds for 3.x configuration files.
  • Code wins when a setting is given twice: a UsePersistentStore in code beats a leftover quartz.jobStore.type, and ConfigureScheduler beats appsettings.json. The built-in fallbacks (the driver delegate and the serializer) are registered last, so they apply only when nothing else claimed the slot.

Unknown job store keys

A persistent store refuses, by name, a key under quartz.jobStore that nothing reads, when it resolves its settings (in the startup validation UsePersistentStore declares, or when the store is built):

Unknown configuration property 'quartz.jobStore.dbRetryIntreval'. It is not a setting of the ADO.NET
job store, and no other reader consults it. Set 'quartz.checkConfiguration' to false to allow keys
Quartz does not read.

(3.x failed on such a key too. 4.0 ignored it, so a typo, or a key a newer 3.x line added that 4.x did not yet translate, left the default in force silently.)

Counted as read:

  • every flat key in the tables above;
  • every property of AdoJobStoreOptions and its Clustering sub-section, spelled as the options type spells it;
  • every quartz.jobStore.lockHandler.* key, written onto the lock handler by name, which reports an unknown one itself;
  • quartz.jobStore.driverDelegateInitString, whose contents are checked as they are parsed.

Case does not matter: quartz.jobstore.tableprefix is quartz.jobStore.tablePrefix.

Not covered:

  • A store with no options type of its own (yours, or another package's) still has leftover keys under this prefix written onto it by name, so an unknown one fails there, with the same message.
  • An in-memory store refuses nothing: it reads one key under this prefix, and the others are not its settings.
Help us by improving this page!
Last Updated: 9/30/26, 6:00 AM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Tutorial
Next
JSON Configuration