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

AddQuartz(string name, ...) registers a named scheduler. Each gets its own configuration, jobs, triggers, listeners and calendars, through the usual DI fluent API. ISchedulerRepository tracks built schedulers by name.

Tips

Without Microsoft DI, build each scheduler from its own QuartzSchedulerBuilder, with Create(q => q.ConfigureScheduler(options => options.InstanceName = ...)), and call BuildScheduler() on each.

When to Use Named Schedulers

  • Different job stores: in-memory for transient jobs, a persistent store for durable jobs.
  • Workload isolation: critical jobs and background maintenance on independent thread pools.
  • Different configurations: misfire thresholds, batch sizes or clustering settings.

Basic Configuration

Give each AddQuartz(string name, ...) call a unique name:

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(TimeSpan.FromSeconds(30)));
});

// 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 inside a named AddQuartz call apply 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
});

Injecting a Named Scheduler

The scheduler's name is its service key, so inject it as a keyed service:

public class MyService
{
    private readonly IScheduler scheduler;

    public MyService([FromKeyedServices("FastScheduler")] IScheduler scheduler)
    {
        this.scheduler = scheduler;
    }

    public async Task DoWork()
    {
        await scheduler.TriggerJob(new JobKey("my-job"));
    }
}

Or resolve it directly:

var fast = provider.GetRequiredKeyedService<IScheduler>("FastScheduler");
var standard = provider.GetRequiredService<IScheduler>();   // the default scheduler, if one is registered

Every part of a named scheduler is registered under its key, for example GetRequiredKeyedService<ISchedulerFactory>("FastScheduler"). Unkeyed registrations belong to the default scheduler.

Warning

The injected IScheduler is a handle that builds the scheduler on first use, because building is asynchronous and a container constructs synchronously.

  • Asynchronous members await the build and are always safe.
  • Status, SchedulerInstanceId, Context and ListenerManager throw InvalidOperationException if reading them would have to build the scheduler.
  • SchedulerName never builds anything.

Under AddQuartzHostedService() the synchronous members are safe once the host has started: the hosted service builds every scheduler while the host starts, before your code runs. It starts them later, once ApplicationStarted fires, unless AwaitApplicationStarted is off.

Finding a scheduler at runtime

When the name is known only at runtime, for example a dashboard or a request naming its scheduler, use the container's ISchedulerRepository. It holds every scheduler that has been built:

public class MyService
{
    private readonly ISchedulerRepository schedulerRepository;

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

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

        // Or every scheduler this container has built
        var all = schedulerRepository.LookupAll();
    }
}

Warning

  • The repository holds only built schedulers, so during startup it may not hold them all. Injecting by key does not have this problem: the handle builds the scheduler it names.
  • The repository is per container, not per process. A scheduler built by its own QuartzSchedulerBuilder is not in it; see the migration guide.

For what the container has registered, built or not, resolve ISchedulerRegistry and call QuerySchedulers(). It returns one SchedulerRegistration per registration, plus one per scheduler bound into the repository without a registration. A scheduler not yet created has a null Status and is not created. Use it for an inventory; use LookupAll for the live schedulers.

Mixing Default and Named Schedulers

The unnamed AddQuartz() and named schedulers can be combined:

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

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

Call order does not matter. The hosted service resolves schedulers when the host starts, so it starts every registered scheduler whether AddQuartz was called before or after it. A container with no scheduler is reported at startup.

Configuration via appsettings.json

Pass the root Quartz section; a named scheduler's settings are read from Schedulers:{name}:

builder.AddQuartz("DurableScheduler");
// or, naming the section yourself:
builder.Services.AddQuartz("DurableScheduler", builder.Configuration.GetSection("Quartz"));

AddQuartzSchedulers registers one named scheduler per child of Schedulers:

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

Flat keys with no typed option, such as a plugin's own settings, go in the named options' Properties dictionary:

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

Per-Scheduler Startup and Shutdown

AddQuartzHostedService(configure) configures every scheduler. A named call overrides it for one scheduler, in either call order:

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

Configuring every scheduler at once

ConfigureAllQuartzSchedulers(configure) applies a builder callback to every scheduler registered through AddQuartz, AddQuartz(name, …) or AddQuartzSchedulers, before or after the call.

  • Each scheduler gets its own instance of what the callback adds: a plugin added to three schedulers is three plugin instances.
  • Remote schedulers from AddQuartzHttpClient have no builder and are skipped.

See Multi-Tenancy.

Limitations

  • Scheduler names must be unique, compared ignoring case.

Job types are not a limitation. AddJob<T> registers the type unkeyed, so one job class can serve every scheduler. AddJobType<TJob, TImplementation>(), AddJobType<TJob>(lifetime) and AddJobType<TJob>(factory) register under one scheduler's key; the job factory checks that key before the container's unkeyed registration. Two schedulers can therefore build the same job type differently. See Multi-Tenancy.

Help us by improving this page!
Last Updated: 9/30/26, 6:00 AM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Microsoft DI Integration
Next
Observability