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

FAQ

Tips

Having problems? Check the Troubleshooting Guide for common issues and solutions.

General Questions

What is Quartz

Quartz is a job scheduling system that can be integrated with, or used along side virtually any other software system. The term "job scheduler" seems to conjure different ideas for different people. As you read this tutorial, you should be able to get a firm idea of what we mean when we use this term, but in short, a job scheduler is a system that is responsible for executing (or notifying) other software components when a pre-determined (scheduled) time arrives.

Quartz is quite flexible, and contains multiple usage paradigms that can be used separately or together, in order to achieve your desired behavior, and enable you to write your code in the manner that seems most 'natural' to your project.

Quartz is very light-weight, and requires very little setup/configuration - it can actually be used 'out-of-the-box' if your needs are relatively basic.

Quartz is fault-tolerant, and can persist ('remember') your scheduled jobs between system restarts.

Although Quartz is extremely useful for simply running certain system processes on given schedules, the full potential of Quartz can be realized when you learn how to use it to drive the flow of your application's business processes.

What is Quartz - From a Software Component View?

Quartz is distributed as a small dynamically linked library (.dll file) that contains all of the core Quartz functionality. The main interface (API) to this functionality is the Scheduler interface. It provides simple operations such as scheduling/unscheduling jobs, starting/stopping/pausing the scheduler.

If you wish to schedule your own software components for execution they must implement the simple Job interface, which contains the method execute(). If you wish to have components notified when a scheduled fire-time arrives, then the components should implement either the TriggerListener or JobListener interface.

The main Quartz 'process' can be started and ran within your own application, or a stand-alone application (with an remote interface).

Why not just use System.Timers.Timer?

.NET Framework has "built-in" timer capabilities, through the System.Timers.Timer class - why would someone use Quartz rather than these standard features?

There are many reasons! Here are a few:

  • Timers have no persistence mechanism.
  • Timers have inflexible scheduling (only able to set start-time & repeat interval, nothing based on dates, time of day, etc.
  • Timers don't utilize a thread-pool (one thread per timer)
  • Timers have no real management schemes - you'd have to write your own mechanism for being able to remember, organize and retrieve your tasks by name, etc.

...of course to some simple applications these features may not be important, in which case it may then be the right decision not to use Quartz.NET.

Miscellaneous Questions

How many jobs is Quartz capable of running?

This is a tough question to answer... the answer is basically "it depends".

I know you hate that answer, so here's some information about what it depends "on".

First off, the JobStore that you use plays a significant factor. The RAM-based JobStore is MUCH (1000x) faster than the ADO.NET-based JobStore. The speed of AdoJobStore depends almost entirely on the speed of the connection to your database, which data base system that you use, and what hardware the database is running on. Quartz actually does very little processing itself, nearly all of the time is spent in the database. Of course RAMJobStore has a more finite limit on how many Jobs & Triggers can be stored, as you're sure to have less RAM than hard-drive space for a database. You may also look at the FAQ "How do I improve the performance of AdoJobStore?"

So, the limiting factor of the number of Triggers and Jobs Quartz can "store" and monitor is really the amount of storage space available to the JobStore (either the amount of RAM or the amount of disk space).

Now, aside from "how many can I store?" is the question of "how many jobs can Quartz be running at the same moment in time?"

One thing that CAN slow down Quartz itself is using a lot of listeners (TriggerListeners, JobListeners, and SchedulerListeners). The time spent in each listener obviously adds into the time spent "processing" a job's execution, outside of actual execution of the job. This doesn't mean that you should be terrified of using listeners, it just means that you should use them judiciously - don't create a bunch of "global" listeners if you can really make more specialized ones. Also don't do "expensive" things in the listeners, unless you really need to. Also be mindful that many plug-ins (such as the "history" plugin) are actually listeners.

The actual number of jobs that can be running at any moment in time is limited by the size of the thread pool. If there are five threads in the pool, no more than five jobs can run at a time. Be careful of making a lot of threads though, as the VM, Operating System, and CPU all have a hard time juggling lots of threads, and performance degrades just because of all of the management. In most cases performance starts to tank as you get into the hundreds of threads. Be mindful that if you're running within an application server, it probably has created at least a few dozen threads of its own!

Aside from those factors, it really comes down to what your jobs DO. If your jobs take a long time to complete their work, and/or their work is very CPU-intensive, then you're obviously not going to be able to run very many jobs at once, nor very many in a given spanse of time.

Finally, if you just can't get enough horse-power out of one Quartz instance, you can always load-balance many Quartz instances (on separate machines). Each will run the jobs out of the shared database on a first-come first-serve basis, as quickly as the triggers need fired.

So here you are this far into the answer of "how many", and I still haven't given you a number And I really hate to, because of all of the variables mentioned above. So let me just say, there are installments of Quartz Java out there that are managing hundreds-of-thousands of Jobs and Triggers, and that at any given moment in time are executing dozens of jobs - and this excludes using load-balancing. With this in mind, most people should feel confident that they can get the performance out of Quartz that they need.

Questions About Jobs

How can I control the instantiation of Jobs?

Implement IJobFactory - Quartz.Spi.IJobFactory in Quartz 3.x, Quartz.Extensibility.IJobFactory in Quartz 4.x - and tell the scheduler to use it.

In Quartz 3.x you assign it to the IScheduler.JobFactory property, or name the type in the quartz.scheduler.jobFactory.type configuration key.

In Quartz 4.x the scheduler has no JobFactory property; the factory is chosen where the scheduler is configured, with UseJobFactory<MyJobFactory>() or UseJobFactory(new MyJobFactory()) on the builder (the same quartz.scheduler.jobFactory.type key still works).

In both versions the Microsoft dependency injection integration installs a job factory of its own, so if all you want is constructor injection into your jobs you do not need to write one.

How do I keep a Job from being removed after it completes?

Build it with JobBuilder.Create<MyJob>().StoreDurably(), which instructs Quartz not to delete the Job when it becomes an "orphan" (when the Job no longer has a Trigger referencing it). IJobDetail.Durable reports the flag; it is set when the detail is built.

How do I keep a Job from firing concurrently?

Quartz.NET 2.x, 3.x, and 4.x

Implement IJob and also decorate your job class with [DisallowConcurrentExecution] attribute. Read the API documentation for DisallowConcurrentExecutionAttribute for more information.

Quartz.NET 1.x

Make the job class implement IStatefulJob rather than IJob. Read the API documentation for IStatefulJob for more information.

How do I stop a Job that is currently executing?

Quartz 1.x and 2.x: See the Quartz.IInterruptableJob interface, and the IScheduler.Interrupt(string, string) method.

Quartz 3.x and 4.x: ask the scheduler to interrupt it — IScheduler.Interrupt(jobKey) for every execution of a job, or one particular firing by its fire instance id (InterruptFireInstance(id) in 4.x, the Interrupt(string) overload in 3.x). Both cancel the token the execution was given, so the job has to co-operate: check IJobExecutionContext.CancellationToken.IsCancellationRequested, or forward the token to what you await, and return early when cancellation is requested. In 4.x the token is also a parameter of Execute, so forwarding it is the default thing to do.

Ask the scheduler to set that token with IScheduler.Interrupt(jobKey), which interrupts every execution of the job, or IScheduler.InterruptFireInstance(fireInstanceId) for one of them. In 4.x, list the executions to pick from with IScheduler.QueryFireInstances(new FireInstanceQuery()) — with a persistent job store that sees the whole cluster, though the interrupt itself is handled by the node running the execution.

Questions About Triggers

How do I chain Job execution? Or, how do I create a workflow?

For the simple case — when this job finishes, run that one — Quartz ships JobChainingJobListener. Register it with the pairs you want chained, and it triggers the second job when the first completes:

JobChainingJobListener chain = new("chain");
chain.AddJobChainLink(new JobKey("extract"), new JobKey("transform"));
chain.AddJobChainLink(new JobKey("transform"), new JobKey("load"));

scheduler.ListenerManager.AddJobListener(chain);

The links live in memory with the listener, so they are re-registered on every start, and the second job is fired rather than scheduled — there is no trigger to see in the store.

In 4.x one job can be chained to several follow-ups, which is a fan-out rather than a chain: call AddJobChainLink again with the same first job, or name them all at once with AddJobChainLinks. Each follow-up is triggered as a firing of its own, so they run concurrently — as many at a time as the thread pool has threads — rather than one after another:

JobChainingJobListener chain = new("chain");
chain.AddJobChainLinks(new JobKey("transform"), [new JobKey("load-warehouse"), new JobKey("load-cache")]);
chain.AddJobChainLink(new JobKey("transform"), new JobKey("notify"));

scheduler.ListenerManager.AddJobListener(chain);

A follow-up that has to wait for one of its siblings is a link from that sibling, not a second link from the same job. Chaining the same follow-up to one job twice is rejected, since it would fire that job twice for a single completion. On 3.x, a second link from the same first job throws instead.

For anything more than that, there is no "direct" way to chain triggers with Quartz, but there are several ways to accomplish it without much effort. Below is an outline of a couple of approaches:

One way is to use a listener (i.e. a TriggerListener, JobListener or SchedulerListener) that can notice the completion of a job/trigger and then immediately schedule a new trigger to fire. This approach can get a bit involved, since you'll have to inform the listener which job follows which, and you may need to worry about persistence of this information.

Another way is to build a Job that contains within its JobDataMap the name of the next job to fire, and as the job completes (the last step in its Execute() method) have the job schedule the next job. Several people are doing this and have had good luck. Most have made a base (abstract) class that is a Job that knows how to get the job name and group out of the JobDataMap using special keys (constants) and contains code to schedule the identified job. Then they simply make extensions of this class that included the additional work the job should do.

In the future, Quartz will provide a much cleaner way to do this, but until then, you'll have to use one of the above approaches, or think of yet another that works better for you.

Why isn't my trigger firing?

The most common reason for this is not having called Scheduler.Start(), which tells the scheduler to start firing triggers.

The second most common reason is that the trigger or trigger group has been paused.

Daylight Saving Time and Triggers

CronTrigger and SimpleTrigger each handle daylight savings time in their own way - each in the way that is intuitive to the trigger type.

First, as a review of what daylight savings time is, please read this resource: http://webexhibits.org/daylightsaving/g.html . Some readers may be unaware that the rules are different for different nations/contents. For example, the 2005 daylight savings time starts in the United States on April 3, but in Egypt on April 29. It is also important to know that not only the dates are different for different locals, but the time of the shift is different as well. Many places shift at 2:00 am, but others shift time at 1:00 am, others at 3:00 am, and still others right at midnight.

SimpleTrigger allows you to schedule jobs to fire every N milliseconds. As such, it has to do nothing in particular with respect to daylight savings time in order to "stay on schedule" - it simply keeps firing every N milliseconds. Regardless your SimpleTrigger is firing every 10 seconds, or every 15 minutes, or every hour or every 24 hours it will continue to do so. However the implication of this which confuses some users is that if your SimpleTrigger is firing say every 12 hours, before daylight savings switches it may be firing at what appears to be 3:00 am and 3:00 pm, but after daylight savings 4:00 am and 4:00 pm. This is not a bug

  • the trigger has kept firing exactly every N milliseconds, it just that the "name" of that time that humans impose on that moment has changed.

CronTrigger allows you to schedule jobs to fire at certain moments with respect to a "Gregorian calendar". Hence, if you create a trigger to fire every day at 10:00 am, before and after daylight savings time switches it will continue to do so. However, depending on whether it was the Spring or Autumn daylight savings event, for that particular Sunday, the actual time interval between the firing of the trigger on Sunday morning at 10:00 am since its firing on Saturday morning at 10:00 am will not be 24 hours, but will instead be 23 or 25 hours respectively.

There is one additional point users must understand about CronTrigger with respect to daylight savings, and it is worth reading carefully because Java Quartz's own page says something Quartz.NET does not do. A CronTrigger is never skipped by a daylight saving transition, and never fires twice for one scheduled occurrence. What a transition decides is which instant a wall-clock time that is missing or repeated resolves to, and the answer depends on whether the expression names a fixed time of day or an interval.

A fixed-time expression is one whose second, minute and hour fields are plain values or comma lists of plain values - 0 15 2 * * ?, or 0 0,30 2 * * ?. Say you are in the United States, where daylight saving events occur at 2:00 am, and you have a CronTrigger that fires every day at 2:15 am:

  • On the day daylight saving time begins, 2:15 am does not exist. The trigger fires once anyway. On 4.x it fires at the end of the gap - 3:00 am, the instant the clocks moved - which is the one in-gap instant the expression itself matches, so IsSatisfiedBy agrees with the fire time. On 3.x the fire is shifted forward by the transition's delta instead, to 3:15 am. A zone whose delta is not a whole hour - Australia/Lord_Howe - splits the same two ways, on its own numbers. An expression matching several of the swallowed wall clocks, such as 0 0,15,30,45 2 * * ?, still fires once: they all name the same instant.
  • On the day daylight saving time ends, 2:15 am occurs twice. The trigger fires once, at the first of the two occurrences. This is the same on both versions.

An interval expression - one with a wildcard, a step or a range in the second, minute or hour field, such as a trigger that fires every 15 minutes of every hour of every day - is where the versions differ most. On 4.x it keeps firing through the repeated hour, so both passes of it run. On 3.x the repeated hour is fired only once, so on the day daylight saving time ends you have an hour of real time in which no firing occurs: when 2:00 am arrives it becomes 1:00 am again, all the firings of the one o'clock hour have already happened, and the trigger's next fire time was already 2:00 am. Over a spring-forward gap on 4.x the gap-end rule shows as an extra fire rather than a moved one - an hourly 0 30 * * * ? fires at 3:00 for the occurrence the gap swallowed and again at 3:30 for the next hour's, where 3.x resumes from the shifted 3:30 and fires once.

In summary, all of this makes perfect sense, and should be easy to remember if you keep these two rules in mind:

  • SimpleTrigger ALWAYS fires exactly every N seconds, with no relation to the time of day.
  • CronTrigger ALWAYS fires at a given time of day and then computes its next time to fire. If that time of day does not exist on a given day, it fires at the one instant the transition maps that wall clock onto rather than being skipped. If the time occurs twice in a given day, a fixed-time expression fires once, at the first occurrence, because after firing it computes the next time of day to fire on; an interval expression fires through both occurrences on 4.x.

Whatever the schedule, name the time zone. A trigger with no zone uses TimeZoneInfo.Local, so the same expression means two different things on a developer's machine and in a container. 4.x's Cron Expression Reference states the two rules on their own, and Daylight saving, clock changes and cluster skew covers choosing the trigger family that means what you meant.

System clock changes (NTP corrections, manual adjustments)

Daylight saving transitions are not the only way the wall clock can move. An NTP correction, a manual clock change, or a virtual machine being suspended and resumed can all shift the system clock by an arbitrary amount, in either direction.

Quartz always schedules against the wall clock, so moving the clock backward by an hour means a trigger whose next fire time was already computed will not fire until the clock has caught up again - the hour has to be lived through a second time. This is expected: the trigger's next fire time is a point on the calendar, not an offset from "now".

What is not expected is the scheduler failing to notice once the clock is restored. Quartz therefore never waits on a single unbounded sleep derived from the wall clock; it re-evaluates the current time at bounded intervals, so after any clock change it resumes on its own:

  • the firing loop recovers within one quartz.scheduler.idleWaitTime (30 seconds by default);
  • with AdoJobStore, misfire handling recovers within one quartz.jobStore.misfireHandlerFrequency, and cluster check-in within one quartz.jobStore.clusterCheckinInterval.

Recovery is bounded, not instantaneous - allow up to one of those intervals before triggers resume after the clock has been corrected. If you are deliberately testing clock changes, prefer faking the clock (see TimeProvider) over changing the machine clock, so that only the wall clock moves and monotonic timers are unaffected.

Questions About AdoJobStore

How do I improve the performance of AdoJobStore?

There are a few known ways to speed up AdoJobStore, only one of which is very practical.

First, the obvious, but not-so-practical:

  • Buy a better (faster) network between the machine that runs Quartz, and the machine that runs your RDBMS.
  • Buy a better (more powerful) machine to run your database on.
  • Buy a better RDBMS.

Secondly, use driver delegate implementation that is specific to your database, like SQLServerDelegate, for best performance.

Tips

You should also always prefer the latest version of the library. Quartz.NET 2.0 is much more efficient than 1.x series and 2.2.x line again has AdoJobStore related performance improvements over earlier 2.x releases.

Quartz in web environment

Scheduler keeps stopping when application pool gets recycled

By default IIS recycles and stops app pools from time to time. This means that even if you have Application_Start event to start Quartz when web app is being first accessed, the scheduler might get disposed later on due to site inactivity.

If you have IIS 8 available, you can configure your site to be pre-loaded and kept running. See this blog post for details.

For more detailed guidance on web environments, IIS, and hosted services, see the Troubleshooting Guide.

Quartz.NET 4.x Questions

What .NET version does Quartz 4.x require?

Quartz.NET 4.x targets .NET 10.0. You must be running at least .NET 10.0 to use Quartz 4.x.

Can I use Task instead of ValueTask in Quartz 4.x?

Quartz 4.x changed all Task return types to ValueTask. Your IJob.Execute method must now return ValueTask:

public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
    // your job logic
}

If you need Task semantics elsewhere (e.g., to await a result multiple times), call .AsTask() on the ValueTask once and work with the resulting Task.

For more details, see the Migration Guide.

What happened to Quartz.Extensions.DependencyInjection and Quartz.Extensions.Hosting?

These packages have been merged into the main Quartz package in 4.x. You can remove the separate package references. The AddQuartz() and AddQuartzHostedService() extension methods are now available directly from the Quartz package.

How do I replace SystemTime in Quartz 4.x?

SystemTime was removed in 4.x. Use the .NET TimeProvider abstraction instead:

QuartzSchedulerBuilder builder = QuartzSchedulerBuilder.Create(q => q.UseTimeProvider(new FakeTimeProvider()));

This is particularly useful for unit testing where you need to control the passage of time.

Help us by improving this page!
Last Updated: 9/19/26, 9:30 PM
Contributors: Marko Lahma, Claude Opus 5 (1M context)
Prev
Comparison
Next
Best Practices