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

    • Quartz 3 Quick Start
    • Tutorial
      • Using Quartz
      • Library Overview
      • Jobs And Triggers
      • More About Jobs
      • More About Triggers
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Stores
      • Tuning the Scheduler
      • Configuration, Resource Usage and SchedulerFactory
      • Advanced (Enterprise) Features
    • Configuration Reference
    • JSON Configuration
    • Frequently Asked Questions
    • Best Practices
    • Troubleshooting
    • API Documentation
    • Database Schema
    • Database Schema Changes
    • Migration Guide
    • Miscellaneous Features
  • How To's

    • One-Off Job
    • Multiple Triggers
    • Job Template
    • Using the CronTrigger
    • Rescheduling Jobs
  • Packages

    • Quartz Core Additions

      • Dashboard
      • Jobs
      • Serialization (System.Text.Json)
      • Serialization (Newtonsoft Json.NET)
      • Plugins
    • Integrations

      • ASP.NET Core Integration
      • Hosted Services Integration
      • Microsoft DI Integration
      • Multiple Schedulers with Microsoft DI
      • OpenTelemetry Integration
      • OpenTracing Integration
      • Redis Lock Handler
      • TimeZoneConverter Integration
    • 3rd Party Plugins for Quartz
  • Unreleased Releases

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

        • Quartz Core Additions

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

          • ASP.NET Core Integration
          • HTTP API
          • Dashboard
          • Hosted Services Integration
          • Microsoft DI Integration
          • Multiple Schedulers with Microsoft DI
          • OpenTelemetry Integration
          • Redis Lock Handler
          • TimeZoneConverter Integration
        • 3rd Party Plugins for Quartz
  • Old Releases

    • Quartz 2.x
      • Quartz 2 Quick Start
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
        • CronTrigger Tutorial
      • Configuration Reference
      • Migration Guide
      • API Documentation
    • Quartz 1.x
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
      • API Documentation
  • License

Quartz.NET has always supported running multiple schedulers in a single process -- each QuartzSchedulerBuilder builds an independent scheduler, and an ISchedulerRepository tracks by name the schedulers built alongside it. However, configuring multiple schedulers through the Microsoft DI AddQuartz() API required workarounds because the registration model was designed around a single scheduler per container.

The named AddQuartz(string name, ...) overload makes this first-class: each named scheduler gets its own isolated configuration, jobs, triggers, listeners, and calendars, all managed through the familiar DI fluent API.

Tips

If you are not using Microsoft DI, you can create multiple schedulers from separate QuartzSchedulerBuilders, each given its own ConfigureScheduler(options => options.InstanceName = ...), and call BuildScheduler() on each.

When to Use Named Schedulers

  • Different job stores -- one scheduler uses in-memory storage for transient jobs, another uses a persistent database store for durable jobs
  • Workload isolation -- separate critical jobs from background maintenance tasks with independent thread pools
  • Different configurations -- schedulers with different misfire thresholds, batch sizes, or clustering settings

Basic Configuration

Register each scheduler with a unique name using the AddQuartz(string name, ...) overload:

var builder = Host.CreateApplicationBuilder(args);

// First scheduler: fast in-memory jobs
builder.Services.AddQuartz("FastScheduler", q =>
{
    q.UseInMemoryStore();
    q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 5);

    q.ScheduleJob<NotificationJob>(trigger => trigger
        .WithIdentity("notify-trigger")
        .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromSeconds(30)).RepeatForever()));
});

// Second scheduler: persistent database jobs
builder.Services.AddQuartz("DurableScheduler", q =>
{
    q.UsePersistentStore(s =>
    {
        s.UseSqlServer(sqlServer =>
        {
            sqlServer.ConnectionString = "your connection string";
        });
        s.UseSystemTextJsonSerializer();
    });

    q.ScheduleJob<ReportJob>(trigger => trigger
        .WithIdentity("report-trigger")
        .WithCronSchedule("0 0 2 * * ?"));
});

// Single call starts all named schedulers
builder.Services.AddQuartzHostedService(options =>
{
    options.WaitForJobsToComplete = true;
});

builder.Build().Run();

Per-Scheduler Listeners and Calendars

Listeners and calendars registered within a named AddQuartz call are scoped to that scheduler only:

builder.Services.AddQuartz("Scheduler1", q =>
{
    q.AddSchedulerListener<AuditSchedulerListener>();
    q.AddJobListener<LoggingJobListener>();
    q.AddTriggerListener<MetricsTriggerListener>();

    q.AddCalendar<HolidayCalendar>("holidays", new AddCalendarOptions { Replace = true, UpdateTriggers = true },
        cal => cal.AddExcludedDay(new DateOnly(2025, 12, 25)));
    // These listeners and calendars only apply to Scheduler1
});

builder.Services.AddQuartz("Scheduler2", q =>
{
    // Scheduler2 has no listeners or calendars unless explicitly added here
});

Accessing Named Schedulers Programmatically

Every scheduler registered in a container is bound into that container's ISchedulerRepository, so you can retrieve any of them by name from the repository:

public class MyService
{
    private readonly ISchedulerRepository schedulerRepository;

    public MyService(ISchedulerRepository schedulerRepository)
    {
        this.schedulerRepository = schedulerRepository;
    }

    public async Task DoWork()
    {
        // Get a specific named scheduler
        var scheduler = schedulerRepository.Lookup("FastScheduler");
        if (scheduler != null)
        {
            await scheduler.TriggerJob(new JobKey("my-job"));
        }

        // Or get all schedulers
        var all = schedulerRepository.LookupAll();
    }
}

If you also have a default scheduler (registered via unnamed AddQuartz()), you can inject ISchedulerFactory and use LookupScheduler(name):

public class MyService
{
    private readonly ISchedulerFactory schedulerFactory;

    public MyService(ISchedulerFactory schedulerFactory)
    {
        this.schedulerFactory = schedulerFactory;
    }

    public async Task DoWork()
    {
        var scheduler = await schedulerFactory.LookupScheduler("FastScheduler");
    }
}

Warning

Named schedulers are only available after the hosted service has created and started them. During application startup, they may not yet be in the repository.

ISchedulerFactory is only available from DI when a default (unnamed) AddQuartz() call has been made. If you only use named schedulers, inject ISchedulerRepository instead.

The repository is scoped to the container, not the process. A scheduler built by a QuartzSchedulerBuilder of its own is not in it -- see the migration guide.

Mixing Default and Named Schedulers

You can combine the traditional unnamed AddQuartz() with named schedulers:

// Default scheduler (traditional single-scheduler usage)
builder.Services.AddQuartz(q =>
{
    q.ScheduleJob<MainJob>(trigger => trigger
        .WithIdentity("main-trigger")
        .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMinutes(1)).RepeatForever()));
});

// Additional named scheduler
builder.Services.AddQuartz("Auxiliary", q =>
{
    q.ScheduleJob<CleanupJob>(trigger => trigger
        .WithIdentity("cleanup-trigger")
        .WithCronSchedule("0 0 3 * * ?"));
});

// Starts both the default and the named scheduler
builder.Services.AddQuartzHostedService();

Tips

The order of the calls does not matter. The hosted service resolves the schedulers when the host starts, so it starts every scheduler registered in the container whether AddQuartz was called before it or after. A container with no scheduler at all is reported at startup rather than starting nothing silently.

Configuration via appsettings.json

A named scheduler's configuration can come from a section. Pass the root Quartz section and the scheduler's own settings are resolved out of Schedulers:{name}:

builder.Services.AddQuartz("DurableScheduler", builder.Configuration.GetSection("Quartz"));

To register every scheduler the section describes rather than one of them, call AddQuartzSchedulers, which registers one named scheduler per child of Schedulers:

builder.Services.AddQuartzSchedulers(builder.Configuration.GetSection("Quartz"));
{
  "Quartz": {
    "Schedulers": {
      "DurableScheduler": {
        "Scheduler": {
          "InstanceId": "AUTO"
        },
        "JobStore": {
          "Type": "Quartz.Impl.AdoJobStore.LocalTransactionJobStore, Quartz"
        }
      }
    }
  }
}

Individual flat keys that no typed option covers can still be set on the named options directly, through the Properties dictionary:

builder.Services.Configure<QuartzOptions>("DurableScheduler",
    options => options.Properties["quartz.jobStore.someThirdPartySetting"] = "value");

Per-Scheduler Startup and Shutdown

AddQuartzHostedService(configure) configures every scheduler, which is what it has always meant. A scheduler that has to differ says so by name, and its settings refine the shared ones whichever order the two calls are made in:

// shared by every scheduler
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

// ...except this one, which waits longer before its first fire
builder.Services.AddQuartzHostedService("DurableScheduler", options =>
{
    options.StartDelay = TimeSpan.FromMinutes(2);
});

Limitations

  • Job types are shared -- job classes are resolved from the shared DI container. The same job type can be used across multiple schedulers.
  • Scheduler names must be unique -- each call to AddQuartz(name, ...) must use a distinct name.
Help us by improving this page!
Last Updated: 8/19/26, 7:27 PM
Contributors: Marko Lahma, Claude Fable 5
Prev
Microsoft DI Integration
Next
OpenTelemetry Integration