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

Quartz.HttpClient is the client half of the HTTP API. HttpScheduler is a full IScheduler implementation whose calls go over the wire, so an operator process, a control panel or a deployment script schedules jobs against a remote scheduler with the same code it would use against a local one.

dotnet add package Quartz.HttpClient

What it pairs with

The server has to be running the Quartz HTTP API, from Quartz.AspNetCore:

builder.Services.AddQuartzHttpApi();
// ...
app.MapQuartzHttpApi("/quartz-api");

Two things must line up:

  • The path. The client's HttpClient.BaseAddress plus the API path must reach the endpoints. The simplest arrangement is a base address that already includes the API path.
  • The scheduler name. Every request names the scheduler it is for, and the name the client is registered with must be the remote scheduler's own SchedulerName. A mismatch is a 404, not a connection error.

BaseAddress must end in /; the constructor rejects one that does not, because relative endpoint paths would otherwise resolve against the wrong segment.

Registering the client

The recommended shape names an IHttpClientFactory client, so the handler is pooled and recycled:

builder.Services.AddHttpClient("quartz", client =>
{
    client.BaseAddress = new Uri("https://scheduler.example.com/quartz-api/");
    client.Timeout = TimeSpan.FromSeconds(30);
});

builder.Services.AddQuartzHttpClient(schedulerName: "MyScheduler", httpClientName: "quartz");

There are three overloads:

OverloadUse when
AddQuartzHttpClient(string schedulerName, string httpClientName, JsonSerializerOptions?)the client is registered with AddHttpClient — the normal case
AddQuartzHttpClient(string schedulerName, Func<IServiceProvider, HttpClient> createHttpClient, JsonSerializerOptions?)the client is assembled from other services, or from something the factory does not know about
AddQuartzHttpClient(Action<HttpClientOptions> configure)you want to set several things at once

HttpClientOptions carries SchedulerName, HttpClientName, CreateHttpClient and JsonSerializerOptions. Exactly one of HttpClientName and CreateHttpClient must be set; giving neither or both fails validation at registration, with the same OptionsValidationException every other Quartz options type throws.

CreateHttpClient runs once, when the scheduler is first resolved, and is handed the container. The client it returns belongs to whoever created it — the scheduler never disposes it. That is why the option is a factory rather than an HttpClient: an options object is bound, cached and shared, and a live client sitting in one has no owner.

Injecting it

A remote scheduler is registered exactly like a local one: keyed by its name, and unkeyed as well while it is the only scheduler in the container.

public sealed class OpsController(IScheduler scheduler);                          // one scheduler

Once a second scheduler joins the container, name the one you meant:

public sealed class OpsController([FromKeyedServices("MyScheduler")] IScheduler scheduler);
IScheduler reporting = provider.GetRequiredKeyedService<IScheduler>("reporting");

The unkeyed registration is TryAdd, so a second remote scheduler does not quietly take over what "the scheduler" means — with two of them, inject by key.

Changed in 4.x

Driving two remote schedulers used to need a marker interface of its own, implemented by a type emitted at runtime. The service key says the same thing without the reflection, so the generic AddQuartzHttpClient<TScheduler>() overloads are gone.

Registration also binds the scheduler into the container's ISchedulerRepository, so it shows up in GetAllSchedulers, in the dashboard and in a locally hosted HTTP API. Under a host that happens at startup rather than on first injection; a container with no host stays exactly as lazy as it was.

Constructing one directly

No container needed:

using HttpClient http = new() { BaseAddress = new Uri("https://scheduler.example.com/quartz-api/") };
IScheduler scheduler = new HttpScheduler("MyScheduler", http);

await scheduler.TriggerJob(new JobKey("nightly-report", "reports"));

Authentication

The client carries no authentication of its own — it is an HttpClient, so whatever you would do for any other API works here:

builder.Services.AddHttpClient("quartz", client =>
    {
        client.BaseAddress = new Uri("https://scheduler.example.com/quartz-api/");
    })
    .AddHttpMessageHandler<BearerTokenHandler>()
    .AddStandardResilienceHandler();

Match it on the server with app.MapQuartzHttpApi("/quartz-api").RequireAuthorization(). The API is scheduler control — shutdown, delete, pause-all are all in it — so an unauthenticated endpoint is a remote kill switch.

Serialization must match the server

Both ends speak the Quartz wire format, which is System.Text.Json with Quartz's own converters. The client builds its options from a copy of whatever you pass in, adds those converters to the copy, and leaves your instance untouched — so sharing one JsonSerializerOptions across several clients is safe.

Custom trigger and calendar types need their serializers registered on both sides. The remote scheduler's registrations are invisible from this process, so the client cannot discover them:

SystemTextJsonSerializerRegistry registry = new();
registry.AddTriggerSerializer<MyTrigger>(new MyTriggerSerializer());

IScheduler scheduler = new HttpScheduler("MyScheduler", http, jsonSerializerOptions: null, registry);

Registering through the container instead — the same AddQuartz-side serializer registration the server uses — is picked up automatically, because AddQuartzHttpClient resolves the container-wide registry.

What travels, and what does not

The wire carries data, not objects. Three consequences are worth knowing before you build on this:

Job details are rebuilt. A JobDetailDto carries name, group, job type name, description, Durable, RequestsRecovery, ConcurrentExecutionDisallowed, PersistJobDataAfterExecution and the job data map. GetJobDetail reconstructs a standard job detail from those fields, so a custom IJobDetail implementation on the server comes back as the ordinary one and any behaviour that lived in your type stays on the server.

The job type is a name. It is the assembly-qualified type name as the server has it. The client does not need the type to exist locally to list, pause or trigger a job — only to reason about it.

Enums are names. status, state, repeatIntervalUnit, daysOfWeek — all of them travel as the C# member name, and the names are the contract. Numeric forms are still accepted on input, which is what keeps an older client working.

What is not supported remotely

MemberBehaviour
ListenerManagerthrows SchedulerException — listeners run in the process that executes jobs
UpdateTriggerDetailsthrows NotSupportedException — not exposed by the API yet

Listeners are the important one: a TriggerListener registered on a client would never see anything, because nothing fires here. Register listeners where the scheduler actually runs.

Blocking members

IScheduler has six members that are properties rather than methods, and over HTTP each of them is a request:

SchedulerInstanceId, IsStarted, InStandbyMode, IsShutdown and Context all call the remote scheduler synchronously, blocking the calling thread for the round trip. SchedulerName is the only one that is free — the client already knows it.

Do not touch them on a request path. GetMetadata() is the async member that answers most of the same questions in one call:

SchedulerMetadata metadata = await scheduler.GetMetadata(cancellationToken);

Its IsProxy is true for an HTTP scheduler, and the three type properties — SchedulerTypeName, JobStoreTypeName, ThreadPoolTypeName — are strings, not System.Type. That is what lets the metadata describe a remote scheduler whose types do not exist in this process.

Paging and bulk fetch over the wire

The query family maps straight onto query-string parameters:

PagedResult<TriggerHeader> page = await scheduler.QueryTriggers(new TriggerQuery
{
    Group = GroupMatcher<TriggerKey>.GroupStartsWith("reporting-"),
    State = TriggerState.Error,
    Skip = 0,
    Take = 100,
    IncludeTotalCount = true,
}, cancellationToken);

Skip, Take and IncludeTotalCount become skip, take and includeTotalCount; matchers become groupStartsWith, nameEquals and their siblings. take defaults to 250 at both ends, so a client that leaves it unset gets the same page size the server would have chosen.

QueryFireInstances works the same way and is how a remote console shows what is running — across the whole cluster, since the listing is store-backed.

Bulk fetch posts the keys back:

List<IJobDetail> details = await scheduler.GetJobDetails(keys, cancellationToken);

The endpoint accepts at most 1000 keys per call; page the keys if you have more.

Errors

Anything the server rejects arrives as an HttpClientException, which derives from SchedulerException, with the RFC 7807 problem details in the message. Turning on QuartzHttpApiOptions.IncludeStackTraceInProblemDetails on the server puts the server's stack trace in there too — useful in development, and not something to ship.

A 404 for a read is not an error: GetJobDetail and GetTrigger return null, exactly as a local scheduler would.

See also

  • HTTP API — the server half, and the full endpoint and wire-format reference
  • Querying Jobs and Triggers — the query family these calls implement
  • Multiple Schedulers — naming and keying schedulers in one container
Help us by improving this page!
Last Updated: 8/23/26, 6:42 AM
Contributors: Marko Lahma
Prev
HTTP API
Next
Dashboard