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

Use QuartzSchedulerBuilder when there is no IServiceCollection to register a scheduler with: a console tool, a library, a test, a worker that manages its own lifecycle. It creates its own container, configures the scheduler with the same API as AddQuartz, and returns something you can dispose.

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

await scheduler.Start();

One configuration API, two entry points

Create passes the callback an IQuartzBuilder, the same interface the AddQuartz callback gets, so q => q.ConfigureScheduler(…) is the same method in both places. That covers:

ConfigureScheduler, ConfigureOptions<TOptions>, UseDefaultThreadPool, UseThreadPool<T>, UseInMemoryStore, UsePersistentStore, UseJobStore<T>, UseJobFactory<T>, UseTypeLoader<T>, UseInstanceIdGenerator<T>, UseTimeProvider, UseExecutionLimits, AddPlugin<T>, AddSchedulerListener<T>, AddJobListener<T>, AddTriggerListener<T>, the extension methods AddJob<T>, AddTrigger<TJob>, ScheduleJob<T> and AddCalendar, and every extension a package of your own contributes.

The builder's own members are only Create(configure), Build(), BuildScheduler(), UseConfiguration(IConfiguration) and the two UseProperties overloads. It declares no configuration member of its own, so it cannot fall behind AddQuartz.

The callback runs immediately, and the terminal methods are on the builder it returns, so the whole thing is one expression:

await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder
    .Create(q => q
        .UseInMemoryStore()
        .UseDefaultThreadPool(10))
    .Build();

Build, or BuildScheduler

// I want the scheduler
IScheduler scheduler = await QuartzSchedulerBuilder.Create(q => q.UseInMemoryStore()).BuildScheduler();
// I want to own the lifetime
await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder.Create(q => q.UseInMemoryStore()).Build();
IScheduler scheduler = await factory.GetScheduler();
  • BuildScheduler() is Build().GetScheduler() and discards the factory. Use it when the scheduler lives as long as the process, not when something must clean up.
  • Build() validates the container it creates (ValidateOnBuild, ValidateScopes), so a registration mistake fails there rather than at the first job execution.

The factory owns the container

StandaloneSchedulerFactory is an ISchedulerFactory that also implements IDisposable and IAsyncDisposable. Disposing it shuts the scheduler down, then disposes the service provider and everything that container created: the job store, the thread pool, your registered services. The hosted service uses the same order, because a container disposed under a running scheduler leaves it firing triggers whose jobs it can no longer build.

await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder
    .Create(q => q.UseInMemoryStore())
    .Build();

IScheduler scheduler = await factory.GetScheduler();
await scheduler.Start();

// ... do work ...

// leaving the scope shuts the scheduler down, then disposes the container
  • Prefer await using. The synchronous Dispose() exists for code that cannot be async, and blocks on the same shutdown.
  • The shutdown does not wait for running jobs, the same default as QuartzHostedServiceOptions.WaitForJobsToComplete and IScheduler.Shutdown(). To wait, shut down yourself first; disposal then finds nothing left to shut down:
await scheduler.Shutdown(waitForJobsToComplete: true);
  • Disposing twice does nothing the second time. Disposing a factory whose GetScheduler() was never called does nothing; no scheduler is built just to be torn down.
  • Never disposing is a supported choice. A console application whose scheduler runs until the process ends behaves as the process-lifetime scheduler of earlier versions did. Disposal is for a scheduler shorter-lived than the process: a test, a CLI subcommand, a plug-in host.

Warning

A scheduler that has been shut down is not restarted in place. The container owns its parts' lifetimes, so GetScheduler() after a Shutdown() throws instead of returning a dead instance; build a new factory. Standby() / Start() is the pause-and-resume pair. In an application with a container, ISchedulerRuntime.Restart builds the new one for you from the recipe the old one was built with.

Jobs, triggers and calendars

The IQuartzBuilder extension methods work unchanged and chain inside the callback:

await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder
    .Create(q => q
        .UseInMemoryStore()
        .AddJob<ReportJob>(j => j.WithIdentity("nightly", "reports").StoreDurably())
        .AddTrigger<ReportJob>(t => t
            .ForJob("nightly", "reports")
            .WithIdentity("nightly-trigger", "reports")
            .WithCronSchedule("0 30 2 * * ?"))
        .AddCalendar<HolidayCalendar>("holidays", configure: c => c.AddExcludedDay(new DateOnly(2026, 12, 25))))
    .Build();

Jobs declared this way are registered with the container, so they can take constructor dependencies. q.Services is a real IServiceCollection; register anything there you would register in an application container:

QuartzSchedulerBuilder builder = QuartzSchedulerBuilder.Create(q =>
{
    q.Services.AddSingleton<IReportRenderer, PdfReportRenderer>();
    q.Services.AddHttpClient();
    q.UseInMemoryStore().AddJob<ReportJob>(j => j.WithIdentity("nightly"));
});

Configuration from a file

UseConfiguration is the standalone counterpart of AddQuartz(configuration) and reads the section as a host does: hierarchical Scheduler and ThreadPool sections bind to the typed options, a Schedule section becomes jobs and triggers, and flat quartz.* keys keep their meaning.

IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .Build();

await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder.Create()
    .UseConfiguration(configuration.GetSection("Quartz"))
    .Build();

UseProperties takes flat keys, from a properties file or an environment-derived bag, as StdSchedulerFactory did:

NameValueCollection properties = new()
{
    ["quartz.scheduler.instanceName"] = "reporting",
    ["quartz.threadPool.maxConcurrency"] = "20",
};

QuartzSchedulerBuilder.Create().UseProperties(properties);

Another overload takes IEnumerable<KeyValuePair<string, string?>>, the shape of a Dictionary<string, string?> and of QuartzOptions.Properties.

  • Code wins, whichever order the calls are written in. Values from configuration and properties are applied before anything the builder was told, and implementations they name are registered after, because options are last-wins and registrations first-wins.
  • Property keys are checked against the ones Quartz reads, so a misspelling is reported. Set quartz.checkConfiguration to false when you keep keys of your own in the same bag.

Persistent and clustered, standalone

Persistence needs no host:

await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder
    .Create(q => q
        .ConfigureScheduler(o =>
        {
            o.InstanceName = "orders";
            o.InstanceId = Environment.MachineName;
        })
        .UsePersistentStore(s =>
        {
            s.UseSqlServer(connectionString);
            s.UseClustering(c => c.CheckinInterval = TimeSpan.FromSeconds(10));
            s.ConfigureStore(o => o.TablePrefix = "QRTZ_");
        }))
    .Build();

The dialect methods (UseSqlServer, UsePostgres, UseMySql, UseMySqlConnector, UseSqlite, UseSystemDataSqlite, UseOracle, UseFirebird, UseGenericDatabase) each take a connection string or an Action<DataSourceOptions>.

Tips

Without a host, options validation runs later. ValidateOnStart is wired up either way, but the validator that runs it is a hosted service, so without a host a bad option value is reported the first time the options are read, during GetScheduler(), instead of at application start.

Scheduler isolation

Each Build() creates its own container with its own ISchedulerRepository. Two standalone factories never see each other's schedulers: GetAllSchedulers() on one returns only what it built, and LookupScheduler(name) cannot find the other's. That keeps parallel tests safe.

To share one repository between several entry points, register a shared instance before building. Quartz's own registration is TryAdd, so yours wins:

ISchedulerRepository shared = new SchedulerRepository();

QuartzSchedulerBuilder first = QuartzSchedulerBuilder.Create(q => q.Services.AddSingleton(shared));
QuartzSchedulerBuilder second = QuartzSchedulerBuilder.Create(q => q.Services.AddSingleton(shared));

What the container-first path adds

Container-firstStandalone
AddQuartzHostedService — start, graceful shutdown, WaitForJobsToComplete, StartDelayyou call Start() and Shutdown()
AddQuartz(name, …) — several named schedulers, keyed by nameone scheduler per builder
AddHealthChecks().AddQuartz() / q.AddQuartzHealthChecks()—
AddQuartzHttpApi / the dashboard—
Options validated at application startvalidated on first use
Configuration bound by the hostUseConfiguration(section)
Application services already registeredregister them on q.Services yourself
  • q.SchedulerName inside the callback is "": the standalone builder configures the default scheduler. The instance name comes from ConfigureScheduler(o => o.InstanceName = …).
  • If the process already has a HostApplicationBuilder or WebApplicationBuilder, use AddQuartz (Configuration, Resource Usage and Building a Scheduler, Microsoft DI Integration).

The standalone builder is the entry point for tests; see Testing. Every option, typed and legacy, is in the Configuration Reference.

Coming from 3.x

3.x4.x
StdSchedulerFactory.GetDefaultScheduler()QuartzSchedulerBuilder.Create(q => q.UseInMemoryStore()).BuildScheduler()
new StdSchedulerFactory(properties)QuartzSchedulerBuilder.Create().UseProperties(properties)
DirectSchedulerFactory.Instance.CreateScheduler(…)the Use… members in the callback — that is the direct path
SchedulerBuilder.Create()QuartzSchedulerBuilder.Create(q => …)
quartz.config picked up implicitlyUseConfiguration or UseProperties, explicitly

The main change is ownership: 3.x's factory was a process-wide singleton handing out schedulers that lived forever; 4.x's factory is an object you hold and dispose. The rest are renames.

Help us by improving this page!
Last Updated: 10/6/26, 2:13 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Configuration, Resource Usage and Building a Scheduler
Next
Clustering