# Quartz.NET Misfire Instructions: Every Trigger Type Explained

By Dan Ochoa · Published 2026-10-25 · Updated 2026-10-31
Canonical: https://cronradar.com/blog/quartz-net-misfire-instructions

> What a Quartz.NET misfire is, what misfireThreshold defaults to, when each job store detects one, and what every misfire instruction does per trigger type.

**A Quartz.NET misfire instruction decides what a trigger does when its fire time passed by more than `quartz.jobStore.misfireThreshold` without firing: fire once now, skip to the next scheduled time, or fire every missed time in a row.** Every trigger defaults to `SmartPolicy`. For cron, calendar-interval, and daily-time-interval triggers that means "fire once now"; for simple triggers it depends on the repeat count. You choose another instruction per trigger with a builder method such as `WithMisfireHandlingInstructionDoNothing()`.

Everything below is Quartz.NET 3.x, read from the [3.22.7 source](https://github.com/quartznet/quartznet/tree/v3.22.7). Quartz.NET 4.6.0 keeps the cron trigger logic ([CronTriggerImpl.cs](https://github.com/quartznet/quartznet/blob/v4.6.0/src/Quartz/Impl/Triggers/CronTriggerImpl.cs#L525-L532)) but replaces the builder methods with enums, such as `WithMisfireInstruction(CronTriggerMisfireInstruction.DoNothing)`.

## What Counts as a Misfire

A trigger misfires when the job store finds its next fire time at least `misfireThreshold` in the past. A trigger that is late by less than that simply fires late, and no instruction applies. Three things make a trigger that late:

- **The scheduler wasn't running**, because the process was down or the scheduler was in standby. Only a persistent store remembers fire times across a restart. `RAMJobStore` loses "all scheduling data" when the process ends ([configuration reference](https://www.quartz-scheduler.net/documentation/quartz-3.x/configuration/reference.html#ramjobstore)), so downtime never produces a misfire with it.
- **No worker thread was free.** The default pool runs 10 jobs at once ([TaskSchedulingThreadPool.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Simpl/TaskSchedulingThreadPool.cs#L32)), and the scheduler thread waits for a free thread before acquiring the next trigger ([QuartzSchedulerThread.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Core/QuartzSchedulerThread.cs#L296)). A `[DisallowConcurrentExecution]` job that's still running holds back its own triggers the same way.
- **The trigger was paused.** Resuming it applies its instruction to the fire times it missed ([RAMJobStore.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Simpl/RAMJobStore.cs#L1492-L1545)).

### The Default Threshold Is 60 Seconds, or 5

The configuration reference lists 60000 ms for `quartz.jobStore.misfireThreshold` on both store types ([misfireThreshold](https://www.quartz-scheduler.net/documentation/quartz-3.x/configuration/reference.html#quartz-jobstore-misfirethreshold)). In the source, the database stores' `JobStoreSupport` defaults to one minute ([JobStoreSupport.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/JobStoreSupport.cs#L68)), and the `quartz.config` embedded in the Quartz assembly sets 60000 ([quartz.config](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/quartz.config)). `RAMJobStore`'s own default is 5 seconds ([RAMJobStore.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Simpl/RAMJobStore.cs#L65)), and the embedded file is read only when `StdSchedulerFactory` finds no other configuration ([StdSchedulerFactory.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/StdSchedulerFactory.cs#L266-L310)). `AddQuartz(q => ...)` starts from an empty property set ([ServiceCollectionExtensions.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz.Extensions.DependencyInjection/ServiceCollectionExtensions.cs#L26-L31)), so it never sees that file. These defaults are unchanged since at least 3.4.0:

| How the scheduler is configured                                           | Job store     | Threshold if you don't set it |
| ------------------------------------------------------------------------- | ------------- | ----------------------------- |
| `new StdSchedulerFactory()`, no `quartz.config` file, no properties       | `RAMJobStore` | 60 seconds                    |
| `AddQuartz`, `SchedulerBuilder`, or `new StdSchedulerFactory(properties)` | `RAMJobStore` | 5 seconds                     |
| Any                                                                       | `AdoJobStore` | 60 seconds                    |

Five seconds matters with `DoNothing`: an in-memory cron trigger that waits more than five seconds for a free thread loses that run. Set the threshold to the lateness you still consider on time, explicitly, as the example in the cron section does.

## When Each Job Store Applies the Instruction

`RAMJobStore` checks a trigger in `ApplyMisfire` ([RAMJobStore.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Simpl/RAMJobStore.cs#L1724-L1803)) when the scheduler thread acquires triggers, when a paused trigger resumes, and, since 3.20.1, when a `[DisallowConcurrentExecution]` job finishes and unblocks its triggers ([3.20.1 release notes](https://github.com/quartznet/quartznet/releases/tag/v3.20.1)).

`AdoJobStore` splits the work:

- **Acquisition skips misfired triggers.** A trigger already past the threshold isn't acquired, unless its instruction is `IgnoreMisfirePolicy` ([StdAdoConstants.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/StdAdoConstants.cs#L245-L256)).
- **`Start()` handles all of them at once** in a non-clustered store, before anything fires ([JobStoreSupport.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/JobStoreSupport.cs#L1506-L1522)).
- **A background misfire handler** scans every `MisfireHandlerFrequency`, which defaults to the threshold ([JobStoreSupport.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/JobStoreSupport.cs#L438-L462)), handles up to `quartz.jobStore.maxMisfiresToHandleAtATime` triggers per pass (default 20, [reference](https://www.quartz-scheduler.net/documentation/quartz-3.x/configuration/reference.html#quartz-jobstore-maxmisfirestohandleatatime)), and scans again after 50 ms while more remain ([MisfireHandler.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/MisfireHandler.cs)). A clustered store has no startup pass; this handler picks up startup misfires on its first scan. [Quartz.NET clustering](https://cronradar.com/blog/quartz-net-clustering) covers the rest.

Both stores call `TriggerMisfired` on your trigger listeners before applying the instruction, except for `IgnoreMisfirePolicy` triggers, which never enter misfire handling. The database stores also log "Handling N trigger(s) that missed their scheduled fire-time" at Information level ([JobStoreSupport.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/JobStoreSupport.cs#L1578-L1592)); `RAMJobStore` doesn't log the misfire, so there the listener is the only trace of a skipped run. [Quartz.NET monitoring](https://cronradar.com/blog/quartz-net-monitoring) shows one.

### AddQuartz Can Replace the Trigger Before Anyone Sees the Misfire

With a persistent store, the triggers you declare in `AddQuartz` are applied again each time the scheduler is created, before `Start()`. `OverWriteExistingData` is `true` by default, so each stored trigger is replaced, and the replacement computes its next fire time from its own start time, the moment your app built it ([XMLSchedulingDataProcessor.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Xml/XMLSchedulingDataProcessor.cs#L953-L984), [QuartzScheduler.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Core/QuartzScheduler.cs#L921-L970)). The downtime is gone before misfire handling runs, so no instruction applies.

For declared triggers without an explicit `StartAt`, `ScheduleTriggerRelativeToReplacedTrigger` (default `false`, [SchedulingOptions.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz.Extensions.DependencyInjection/SchedulingOptions.cs)) makes the replacement continue from the stored trigger's last fire time, so the missed time is still due at `Start()` and the instruction applies:

```csharp
builder.Services.Configure<QuartzOptions>(options =>
{
    options.Scheduling.ScheduleTriggerRelativeToReplacedTrigger = true;
});
```

The [Microsoft DI integration page](https://www.quartz-scheduler.net/documentation/quartz-3.x/packages/microsoft-di-integration.html) shows the same `options.Scheduling` block for its sibling settings.

## Cron Trigger Instructions

| Instruction                                      | Builder method (3.x)                             | After a misfire                                                                                          |
| ------------------------------------------------ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `MisfireInstruction.SmartPolicy` (0)             | none; the default                                | Same as `FireOnceNow`                                                                                    |
| `MisfireInstruction.CronTrigger.FireOnceNow` (1) | `WithMisfireHandlingInstructionFireAndProceed()` | Fires once now, then continues at the first scheduled time after now                                     |
| `MisfireInstruction.CronTrigger.DoNothing` (2)   | `WithMisfireHandlingInstructionDoNothing()`      | No run; the next fire time becomes the first scheduled time after now that the trigger's calendar allows |
| `MisfireInstruction.IgnoreMisfirePolicy` (-1)    | `WithMisfireHandlingInstructionIgnoreMisfires()` | Never treated as a misfire; fires at each missed time "as soon as it can," then continues                |

The behavior is in `UpdateAfterMisfire` ([CronTriggerImpl.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/Triggers/CronTriggerImpl.cs#L708-L743)), the constants and the quote in [MisfireInstruction.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/MisfireInstruction.cs#L38-L54), and the builder methods in [CronScheduleBuilder.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/CronScheduleBuilder.cs#L371-L402), all present since at least 3.4.0. The [CronTrigger lesson](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#crontrigger-misfire-instructions) confirms the smart policy mapping.

One version to avoid: Quartz.NET 3.17.0 through 3.18.1 computed `DoNothing` (and simple triggers' `RescheduleNext*`) from now minus the threshold, so a trigger could fire immediately instead of waiting. 3.18.2 restored the documented behavior ([#3100](https://github.com/quartznet/quartznet/pull/3100)).

In a .NET 8 worker project (`dotnet new worker`) with the `Quartz.Extensions.Hosting` package, a time-sensitive daily trigger that should skip a missed morning looks like this:

```csharp
using Quartz;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddQuartz(q =>
{
    // Unset, an in-memory store configured here would use 5 seconds.
    q.MisfireThreshold = TimeSpan.FromSeconds(60);

    var digest = new JobKey("morning-digest");
    q.AddJob<MorningDigestJob>(job => job.WithIdentity(digest));

    q.AddTrigger(trigger => trigger
        .ForJob(digest)
        .WithIdentity("morning-digest-trigger")
        .WithCronSchedule("0 0 9 * * ?", cron => cron
            .InTimeZone(TimeZoneInfo.Utc)
            .WithMisfireHandlingInstructionDoNothing()));
});

builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

builder.Build().Run();

public sealed class MorningDigestJob(ILogger<MorningDigestJob> logger) : IJob
{
    public Task Execute(IJobExecutionContext context)
    {
        logger.LogInformation("Digest scheduled for {Scheduled:O}, started {Started:O}",
            context.ScheduledFireTimeUtc, context.FireTimeUtc);
        return Task.CompletedTask;
    }
}
```

Without the DI integration, the same value is `quartz.jobStore.misfireThreshold = 60000` in `quartz.config`. In a `job_scheduling_data_2_0` XML file, the trigger's `<cron>` element takes `<misfire-instruction>DoNothing</misfire-instruction>` before `<cron-expression>`; the schema accepts `SmartPolicy`, `DoNothing`, `FireOnceNow`, and `IgnoreMisfirePolicy` ([job_scheduling_data_2_0.xsd](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Xml/job_scheduling_data_2_0.xsd#L223-L248)).

## Worked Example: A 15-Minute Trigger Down From 02:00 to 03:10

The trigger is `0 0/15 * * * ?` in UTC, stored in a non-clustered `AdoJobStore` with a 60-second threshold, and it last fired at 01:45. The process stops at 01:59 and starts again at 03:10, missing 02:00, 02:15, 02:30, 02:45, and 03:00. At `Start()`, the store finds the next fire time, 02:00, more than 60 seconds past:

| Instruction                                                   | What runs at 03:10                 | Next scheduled run |
| ------------------------------------------------------------- | ---------------------------------- | ------------------ |
| `SmartPolicy` or `FireOnceNow`                                | One run                            | 03:15              |
| `DoNothing`                                                   | Nothing                            | 03:15              |
| `IgnoreMisfirePolicy`                                         | Five runs, for 02:00 through 03:00 | 03:15              |
| Any, if `AddQuartz` replaced the trigger with default options | Nothing; no misfire is detected    | 03:15              |

_Figure: The trigger last fired at 01:45 and misses five fire times, 02:00 through 03:00, while the process is down. At 03:10, SmartPolicy or FireOnceNow runs once, DoNothing runs nothing, and IgnoreMisfirePolicy runs all five; the hollow circle is each trigger's next scheduled run, 03:15._

The five replayed runs start in quick succession and can overlap unless the job class has `[DisallowConcurrentExecution]`. Each one's `context.ScheduledFireTimeUtc` is its own missed time, because the store reports the fire time it just advanced past ([JobStoreSupport.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/AdoJobStore/JobStoreSupport.cs#L5133-L5184)).

The single `FireOnceNow` run is harder to place. Since 3.17.0, a database store with the optional `MISFIRE_ORIG_FIRE_TIME` column reports the first missed time, 02:00, as its `ScheduledFireTimeUtc`; without the column it equals `FireTimeUtc`, 03:10 ([3.17.0 release notes](https://github.com/quartznet/quartznet/releases/tag/v3.17.0)). `RAMJobStore` reports the original time for its own misfires without any column. Neither value tells the job that four more periods passed.

Had the process been back at 02:00:40, 02:00 would be 40 seconds late, inside the threshold, and would fire late under every instruction.

## Simple Trigger Instructions

`SmartPolicy` on a simple trigger picks one of the others from the repeat count ([SimpleTriggerImpl.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/Triggers/SimpleTriggerImpl.cs#L347-L470)):

- **Repeat count 0** (fires once): `FireNow`.
- **`RepeatForever()`**: `RescheduleNextWithRemainingCount`, so the missed run is skipped, unlike a cron trigger's default.
- **Any other repeat count**: `RescheduleNowWithExistingRepeatCount`.

| Instruction (`MisfireInstruction.SimpleTrigger.*`) | Builder method suffix      | After a misfire                                                                                                      |
| -------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `FireNow` (1)                                      | `FireNow()`                | Fires now. On a repeating trigger it acts as `RescheduleNowWithRemainingRepeatCount`                                 |
| `RescheduleNowWithExistingRepeatCount` (2)         | `NowWithExistingCount()`   | Fires now and restarts the interval from now; missed firings aren't counted, so every remaining repeat still happens |
| `RescheduleNowWithRemainingRepeatCount` (3)        | `NowWithRemainingCount()`  | Fires now and restarts the interval from now; missed firings count as used, so this run can be the last              |
| `RescheduleNextWithRemainingCount` (4)             | `NextWithRemainingCount()` | No run; waits for the next time on the original interval, counting missed firings as used                            |
| `RescheduleNextWithExistingCount` (5)              | `NextWithExistingCount()`  | No run; waits for the next time on the original interval without counting them                                       |
| `IgnoreMisfirePolicy` (-1)                         | `IgnoreMisfires()`         | Fires every missed time in quick succession                                                                          |

Each suffix follows `WithMisfireHandlingInstruction`, as in `WithMisfireHandlingInstructionNowWithExistingCount()` ([SimpleScheduleBuilder.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/SimpleScheduleBuilder.cs#L396-L470)). The two `Now` instructions don't fire if the trigger's end time has passed. The two `Next` instructions both compute fire times from the original start time, so they fire at the same times and finish on the original last fire time; they differ only in `TimesTriggered`.

The `Now` instructions move the cadence. A 15-minute `RepeatForever()` trigger that started at midnight, through the outage above, waits for 03:15 by default. With `NowWithExistingCount()` it fires at 03:10, then 03:25 and 03:40:

```csharp
q.AddTrigger(trigger => trigger
    .ForJob(new JobKey("cache-refresh"))
    .WithIdentity("cache-refresh-trigger")
    .WithSimpleSchedule(simple => simple
        .WithIntervalInMinutes(15)
        .RepeatForever()
        .WithMisfireHandlingInstructionNowWithExistingCount()));
```

## Calendar-Interval, Daily-Time-Interval, and Recurrence Triggers

These three have the cron trigger's options, with the same builder method names: `SmartPolicy` means `FireOnceNow`, plus `DoNothing` and `IgnoreMisfirePolicy` ([CalendarIntervalTriggerImpl.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/Triggers/CalendarIntervalTriggerImpl.cs#L336-L379), [DailyTimeIntervalTriggerImpl.cs](https://github.com/quartznet/quartznet/blob/v3.22.7/src/Quartz/Impl/Triggers/DailyTimeIntervalTriggerImpl.cs#L396-L439)). The RRULE recurrence trigger added in [3.18.0](https://github.com/quartznet/quartznet/releases/tag/v3.18.0) follows the same pattern. Of the three, only calendar-interval triggers can be declared in the XML schema, with the same `<misfire-instruction>` element.

## Choosing an Instruction

- **Catch-up work that covers everything since the last run**, such as a sync or a rollup: `FireOnceNow`, which is also the cron default. Have the job work out what it owes from a stored watermark rather than from the fire time, so one catch-up run, five replays, or a skipped night all end with each period done once. [Idempotent scheduled jobs](https://cronradar.com/blog/idempotent-scheduled-jobs) shows the watermark and the period claims that make a repeat harmless.
- **Work whose moment has passed**, such as a 09:00 digest, a reminder, or a snapshot that only means something at that time: `DoNothing`. The skip is silent, so watch the schedule from outside.
- **One execution per occurrence**, keyed by `ScheduledFireTimeUtc`, such as one export file per 15-minute window: `IgnoreMisfirePolicy`, with `[DisallowConcurrentExecution]` and idempotent writes. Budget for the burst: a one-minute trigger down for a day replays 1,440 runs.
- **Interval triggers that repeat forever**: the default skips. Use `NowWithExistingCount()` for a catch-up run if a shifted cadence is acceptable.

None of this applies to downtime on `RAMJobStore`, or to triggers that `AddQuartz` replaces without relative scheduling. [Missed runs after downtime](https://cronradar.com/blog/missed-runs-after-downtime) compares Quartz.NET's catch-up with cron, systemd, Kubernetes, and Hangfire.

## How CronRadar Sees Each Instruction

CronRadar doesn't read misfire settings. It compares the runs that report against the schedule it expects, so it sees whatever your instruction produced. [CronRadar.Quartz](https://docs.cronradar.com/integrations/quartz) 0.1.0, the version NuGet installs (checked October 25, 2026), discovers your jobs when you call `await scheduler.MonitorAll();` after registering jobs and before `Start()`, and its job listener reports start, complete, or fail for every execution, catch-up runs included. It converts a repeating simple trigger, such as the 15-minute `cache-refresh` trigger, to an approximate five-field schedule (`*/15 * * * *`), but sends a cron trigger's six-field expression unchanged, which CronRadar rejects, so a cron-triggered job such as the 09:00 digest isn't monitored by 0.1.0. CronRadar evaluates schedules in UTC, so for a trigger on another zone, compare CronRadar's next expected run with the trigger's next fire time.

For the worked example, with a five-minute [grace period](https://cronradar.com/blog/cron-grace-period) set as `CronRadar.CronRadar.DefaultGracePeriodSeconds = 300` before `MonitorAll()`, because 0.1.0 re-sends the SDK default on every reconciliation and replaces a value edited in the monitor's settings:

| Time (UTC)     | `SmartPolicy` / `FireOnceNow`                                             | `DoNothing`                                                     |
| -------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------- |
| 02:00          | No check-in: `warning`                                                    | Same                                                            |
| 02:05 to 02:06 | Past grace and at least two minutes late: `critical`; an incident opens   | Same                                                            |
| 03:10          | The catch-up run reports start, then complete: `healthy`, with a recovery | Still `critical`                                                |
| 03:15          | Runs on schedule                                                          | The first run since 01:45 completes: `healthy`, with a recovery |

The overdue check runs once a minute, which is why `critical` arrives in that window. With `IgnoreMisfirePolicy`, the first replayed run's completion recovers the monitor and the other four record as successes; extra runs aren't a CronRadar failure mode. Each run sends two check-ins against a limit of 60 telemetry requests a minute per API key, and a rejected check-in is dropped without failing the job, so a long replay can leave gaps in run history. A `DoNothing` skip caused by a busy thread pool looks the same as downtime: no run arrives, and the monitor goes `warning`, then `critical`.

The integration guide covers Quartz.NET 3.x, 3.4 and later. On Quartz.NET 4.x, wrap the job body with the base `CronRadar` package (1.0.13) instead. In 4.6.0, `Execute` returns `ValueTask` and takes a `CancellationToken`; `DigestSender` stands for your own service:

```csharp
using Quartz;

public sealed class MorningDigestJob(DigestSender sender) : IJob
{
    public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        // The class shares its namespace's name, so qualify it fully.
        await CronRadar.CronRadar.WrapAsync("morning-digest", async () =>
        {
            await sender.SendAsync(cancellationToken);
        }, schedule: "0 9 * * *");
    }
}
```

`WrapAsync` reports start, then complete or fail, and rethrows the job's exception, so Quartz's own handling is unchanged.

## Frequently Asked Questions

### What is the default misfire instruction in Quartz.NET?

`MisfireInstruction.SmartPolicy`, value 0. Cron, calendar-interval, daily-time-interval, and recurrence triggers treat it as `FireOnceNow`. A simple trigger treats it as `FireNow` if it fires once, `RescheduleNextWithRemainingCount` if it repeats forever, and `RescheduleNowWithExistingRepeatCount` otherwise.

### What is misfireThreshold in Quartz.NET?

`quartz.jobStore.misfireThreshold` is how many milliseconds past its fire time a trigger may be before it counts as misfired. It is 60 seconds for the database stores and for a scheduler built from Quartz's embedded `quartz.config`, but 5 seconds for a `RAMJobStore` configured through `AddQuartz` or `SchedulerBuilder` without the key. A trigger late by less than the threshold fires late, and its instruction is never consulted.

### Why did my Quartz job run several times after a restart?

The usual cause is `IgnoreMisfirePolicy` on a persistent trigger, which fires once for every time missed during the outage. A job whose `JobDetail` requests recovery also runs again at startup if it was executing during a hard shutdown, with `context.Recovering` set to true ([More about jobs](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/more-about-jobs.html#other-attributes-of-jobs)). Starting two non-clustered schedulers against the same tables is a third cause; the documentation warns it brings "serious data corruption" and "erratic behavior" ([clustering](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/advanced-enterprise-features.html#clustering)).

### Does DoNothing skip the current run too?

It skips the run that misfired: nothing fires now, and the next fire time becomes the first scheduled time after the moment the misfire is handled. A trigger late by less than the threshold isn't a misfire, so it still fires. On Quartz.NET 3.17.0 through 3.18.1, `DoNothing` could fire immediately; upgrade to 3.18.2 or later.

### How do I get alerted when Quartz skips a misfired run?

A `TriggerMisfired` listener can log it, but a process that's down can't report anything. An external schedule monitor such as CronRadar alerts when an expected run doesn't arrive within its grace period, whichever instruction caused the gap.
