Quartz.NETQuartz.NET
Home
Features
Discussions
NuGet
GitHub
Home
Features
Discussions
NuGet
GitHub
  • Getting Started

    • Quartz 3 Quick Start
    • Tutorial
      • Using Quartz
      • Library Overview
      • Jobs And Triggers
      • More About Jobs
      • More About Triggers
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Stores
      • Tuning the Scheduler
      • Configuration, Resource Usage and SchedulerFactory
      • Advanced (Enterprise) Features
    • Configuration Reference
    • JSON Configuration
    • 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
  • Unreleased Releases

    • Quartz 4.x
      • 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 Stores
        • Configuration, Resource Usage and Building a Scheduler
        • Building a Scheduler Without a Host
        • Clustering
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Testing
      • Configuration Reference
      • JSON Configuration
      • Cron Expression Reference
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Tenancy Patterns
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Troubleshooting
      • API Documentation
      • How To's
        • One-Off Job
        • Rescheduling Jobs
        • Multiple Triggers
        • Job Template
        • 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

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

Not every scheduler lives in a web application. A console tool, a library, a test, a worker that does its own lifecycle management — all of them want a scheduler and none of them has an IServiceCollection to hang it on.

QuartzSchedulerBuilder is for those. It creates a container of its own, configures the scheduler with the same API AddQuartz uses, and hands back something you can dispose.

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

await scheduler.Start();

One configuration API, two entry points

QuartzSchedulerBuilder implements IQuartzBuilder, which is the interface the AddQuartz callback gives you. Every configuration verb is therefore the same verb:

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> — plus the extension methods AddJob<T>, AddTrigger<TJob>, ScheduleJob<T> and AddCalendar.

Learn the configuration API once and it works in both places. Only three members are the builder's own: Build(), BuildScheduler(), UseConfiguration(IConfiguration) and the two UseProperties overloads.

Every configuration member returns QuartzSchedulerBuilder, so the whole thing is one expression:

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

Changed in 4.x

In the 4.0 previews the configuration members returned IQuartzBuilder, so a chain had to be broken up and the builder held in a variable to reach Build(). The returns are covariant now and Create()…Build() is one statement.

Build, or BuildScheduler

Two endings, for two different needs:

// I want the scheduler
IScheduler scheduler = await QuartzSchedulerBuilder.Create().UseInMemoryStore().BuildScheduler();

// I want to own the lifetime
await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder.Create().UseInMemoryStore().Build();
IScheduler scheduler = await factory.GetScheduler();

BuildScheduler() is Build().GetScheduler(), and it drops the factory on the floor — which is fine for a process whose scheduler lives as long as the process, and wrong for anything that has to clean up.

Build() also validates the container it creates (ValidateOnBuild, ValidateScopes), so a registration mistake surfaces 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 disposes the service provider it was built with, which shuts the scheduler down and disposes everything the container created — the job store, the thread pool, your own registered services.

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

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

// ... do work ...

// leaving the scope shuts the scheduler down

Prefer await using. The synchronous Dispose() exists so the type fits using in code that cannot be async, but DisposeAsync is what lets the shutdown actually await.

Never disposing is a supported choice. A console application whose scheduler runs until the process ends behaves exactly as it did with the process-lifetime scheduler of earlier versions. The dispose story exists for the cases where a scheduler is shorter-lived than the process: a test, a CLI subcommand, a plug-in host.

Warning

A scheduler that has been shut down cannot be restarted. The container owns its parts' lifetimes, so GetScheduler() after a Shutdown() throws rather than quietly handing back a dead instance — build a new factory instead. Standby() / Start() is the pause-and-resume pair.

Jobs, triggers and calendars

The IQuartzBuilder extension methods work here unchanged:

await using StandaloneSchedulerFactory factory = QuartzSchedulerBuilder.Create()
    .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:

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

Services is a real IServiceCollection. Anything you would register in an application container you can register here.

Configuration from a file

UseConfiguration is the standalone counterpart of AddQuartz(configuration), and reads the section exactly the way a host does — hierarchical Scheduler and ThreadPool sections bind onto the typed options, a Schedule section becomes jobs and triggers, and flat quartz.* keys still mean what they always meant:

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

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

UseProperties is the flat-key path, for a properties file or an environment-derived bag — the shape StdSchedulerFactory took:

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

QuartzSchedulerBuilder.Create().UseProperties(properties);

There is also an overload taking IEnumerable<KeyValuePair<string, string?>>, which is the shape a Dictionary<string, string?> and QuartzOptions.Properties already have.

Code wins, whichever order the two 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 are first-wins. Applying them where the call happened to appear would make precedence depend on the order you happened to type things.

Property keys are checked against the ones Quartz reads, so a misspelling is reported rather than silently ignored. Set quartz.checkConfiguration to false when you keep keys of your own in the same bag.

Persistent and clustered, standalone

Nothing about persistence needs a host:

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

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

Tips

Options validation behaves slightly differently without a host. ValidateOnStart is wired up either way, but the startup validator that runs it is a hosted service — with no host it never runs, so a bad option value is reported the first time the options are read, during GetScheduler(), instead of at application start. It is still reported; it is just reported a moment later.

Scheduler isolation

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

That is what makes parallel tests safe, and it is occasionally not what you want. When several entry points genuinely must share one repository, register a shared instance before building — Quartz's own registration is TryAdd, so yours wins:

ISchedulerRepository shared = new SchedulerRepository();

QuartzSchedulerBuilder first = QuartzSchedulerBuilder.Create();
first.Services.AddSingleton(shared);

QuartzSchedulerBuilder second = QuartzSchedulerBuilder.Create();
second.Services.AddSingleton(shared);

What the container-first path adds

Standalone gives you a scheduler. AddQuartz in an application container gives you a scheduler plus everything a host makes possible:

Container-firstStandalone
AddQuartzHostedService — start, graceful shutdown, WaitForJobsToComplete, StartDelayyou call Start() and Shutdown()
AddQuartz(name, …) — several named schedulers, keyed by nameone scheduler per builder
AddQuartzHealthChecks—
AddQuartzHttpApi / the dashboard—
Options validated at application startvalidated on first use
Configuration bound by the hostUseConfiguration(section)
Application services already registeredregister them on Services yourself

SchedulerName on the builder is "" — the standalone builder configures the default scheduler, and the instance name comes from ConfigureScheduler(o => o.InstanceName = …).

If a process is already a HostApplicationBuilder or a WebApplicationBuilder, use AddQuartz. The standalone builder is for the processes that are not.

Coming from 3.x

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

The big change is ownership: 3.x's factory was a process-wide singleton handing out schedulers that lived forever, and 4.x's factory is an object you hold and dispose. Everything else is a rename.

See also

  • Configuration, Resource Usage and Building a Scheduler — the container-first path
  • Testing — the standalone builder is the entry point every test uses
  • Configuration Reference — every option, typed and legacy
  • Microsoft DI Integration — AddQuartz in depth
Help us by improving this page!
Last Updated: 8/23/26, 6:42 AM
Contributors: Marko Lahma
Prev
Configuration, Resource Usage and Building a Scheduler
Next
Clustering