Troubleshooting
This guide covers common issues users encounter with Quartz.NET and how to diagnose and resolve them.
Scheduler Stops Executing Jobs
Symptoms: Jobs stop firing after running for hours or days. No error messages in logs. The scheduler appears to be running but no triggers fire.
Common Causes:
Thread pool exhaustion — All worker threads are occupied by long-running jobs. Other jobs queue up and eventually misfire.
- Check
quartz.threadPool.threadCount(default: 10). Increase if you have many concurrent jobs. - Ensure jobs don't block threads indefinitely. Use cancellation tokens and timeouts.
- Consider using
[DisallowConcurrentExecution]to prevent a single slow job from consuming all threads.
- Check
Database connectivity issues — Transient database errors during trigger acquisition can leave the scheduler unable to pick up new triggers.
- Check your database connection string and connection pool configuration.
- Ensure your connection pool size is at least thread count + 3 (see Best Practices).
- Review database server logs for connection timeouts or deadlocks.
Unhandled exceptions in listeners — An exception thrown from a
IJobListener,ITriggerListener, orISchedulerListenercan disrupt the scheduling cycle.- Always wrap listener code in try-catch blocks (see Best Practices).
Diagnosis Steps:
- Enable debug logging for
Quartznamespace to see trigger acquisition activity. - Check
QRTZ_FIRED_TRIGGERStable for jobs that never completed. - Check
QRTZ_TRIGGERStable for triggers stuck in unexpected states (see next section). - Verify the scheduler is still started:
scheduler.IsStartedshould betrue.
Triggers Stuck in ACQUIRED State
Symptoms: Triggers show TRIGGER_STATE = 'ACQUIRED' in the database but never fire. New triggers are not being picked up.
Causes:
- The scheduler instance that acquired the trigger crashed or lost connectivity before it could fire.
- Transient database errors during the fire-and-complete cycle.
Diagnosis:
-- Find stuck triggers
SELECT TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE, NEXT_FIRE_TIME
FROM QRTZ_TRIGGERS
WHERE TRIGGER_STATE = 'ACQUIRED';
-- Find fired triggers that never completed
SELECT * FROM QRTZ_FIRED_TRIGGERS
WHERE STATE = 'ACQUIRED';
Resolution:
- Restart the scheduler — On startup, Quartz performs misfire recovery and will re-evaluate stuck triggers based on their misfire instruction.
- Manual recovery — If a restart is not possible, you can update stuck triggers back to
WAITINGstate:
UPDATE QRTZ_TRIGGERS
SET TRIGGER_STATE = 'WAITING'
WHERE TRIGGER_STATE = 'ACQUIRED'
AND NEXT_FIRE_TIME < :currentTimeInMillis;
Warning
Only perform manual database updates as a last resort. Prefer restarting the scheduler to let Quartz handle recovery properly.
Prevention:
- Ensure adequate database connection pool sizing.
- Use clustered mode if running multiple scheduler instances — it includes automatic recovery for failed nodes.
- Keep jobs short-running to minimize the window for failures.
Misfire Handling
A misfire occurs when a trigger's scheduled fire time passes without the job being executed. This can happen because the scheduler was shut down, there were no available worker threads, or the system was under heavy load.
How It Works
- On startup (and periodically during operation), Quartz scans for triggers whose
NEXT_FIRE_TIMEis older thannow - misfireThreshold. - For each misfired trigger, Quartz applies the trigger's configured misfire instruction.
- The default
misfireThresholdis 60 seconds (configurable viaquartz.jobStore.misfireThreshold).
Misfire Instructions by Trigger Type
| Trigger Type | Instruction | Behavior |
|---|---|---|
| SimpleTrigger | FireNow | Fire immediately, remaining repeat count unchanged |
RescheduleNowWithExistingRepeatCount | Fire now, keep original repeat count | |
RescheduleNowWithRemainingRepeatCount | Fire now, only remaining repeats | |
RescheduleNextWithExistingCount | Skip to next scheduled time, keep original count | |
RescheduleNextWithRemainingCount | Skip to next scheduled time, remaining count | |
| CronTrigger | FireOnceNow | Fire immediately once, then resume schedule |
DoNothing | Skip missed firings, wait for next scheduled time | |
| RecurrenceTrigger | FireOnceNow (default) | Fire immediately once, then resume schedule |
DoNothing | Skip missed firings, wait for next scheduled time |
The default "smart policy" varies by trigger type. For CronTrigger and RecurrenceTrigger, it defaults to FireOnceNow. For SimpleTrigger, it depends on the repeat count configuration.
Tuning
If triggers misfire frequently under normal operation, consider:
- Increasing
quartz.threadPool.threadCountto handle more concurrent jobs. - Increasing
quartz.jobStore.misfireThresholdif slight delays are acceptable. - Splitting high-frequency triggers across multiple scheduler instances using clustering.
Job Deserialization Failures After Refactoring
Symptoms: After renaming a job class, changing its namespace, or moving it to a different assembly, the scheduler throws TypeLoadException or JobPersistenceException on startup.
Cause: The QRTZ_JOB_DETAILS table stores the full type name (including namespace and assembly) in the JOB_CLASS_NAME column. When the type moves, the stored reference no longer resolves.
Resolution:
Update the stored type name in the database:
UPDATE QRTZ_JOB_DETAILS
SET JOB_CLASS_NAME = 'NewNamespace.NewClassName, NewAssembly'
WHERE JOB_CLASS_NAME = 'OldNamespace.OldClassName, OldAssembly';
Prevention:
- Keep job class names and namespaces stable across releases.
- If you must rename, apply the database update as part of your deployment process.
- Consider using the
JobTypeabstraction introduced in Quartz 4.x for more flexible type resolution.
Database Connection Issues
Symptoms: JobPersistenceException with inner SqlException/NpgsqlException, intermittent "Couldn't obtain triggers" errors, or "Object cannot be cast from DBNull" errors.
Common Causes:
Insufficient connection pool size — The connection pool is exhausted under load.
- Recommended minimum: thread pool size + 3.
- For clustered setups, account for the additional cluster management connections.
Connection timeouts — The database is slow to respond or the network is unreliable.
- Increase
CommandTimeoutin your connection string. - Verify network latency between the scheduler and database server.
- Increase
Lock contention — Multiple scheduler instances competing for the same rows.
- Ensure all scheduler instances use the same
quartz.scheduler.instanceNameonly when clustering is enabled. - Never point multiple non-clustered schedulers at the same database tables (see Best Practices).
- Ensure all scheduler instances use the same
Datasource Configuration Example
services.AddQuartz(q =>
{
q.UsePersistentStore(s =>
{
s.UseSystemTextJsonSerializer();
s.UseSqlServer(connectionString);
// Ensure your connection string has an adequate pool size
// e.g., "...;Max Pool Size=25;"
});
});
Scheduler in Web Environments
IIS App Pool Recycling
By default, IIS recycles and stops application pools due to inactivity. This will stop your Quartz scheduler.
Solutions:
IIS 8+: Configure your site as "Always Running" with preload enabled. See Microsoft docs on Application Initialization.
Use the Hosted Service integration (recommended) — Register Quartz as a hosted service so it ties into the ASP.NET Core application lifecycle:
services.AddQuartz(q =>
{
// configure jobs and triggers
});
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
Run as a separate process — For critical scheduling, consider running the scheduler in a Windows Service or Linux systemd service rather than inside a web application.
Graceful Shutdown
When the application shuts down, give jobs time to complete:
services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
Jobs should check IJobExecutionContext.CancellationToken to respond to shutdown requests promptly.
Common Error Messages
| Error | Likely Cause | Resolution |
|---|---|---|
ObjectAlreadyExistsException | Attempting to schedule a job or trigger with a key that already exists | Use scheduler.RescheduleJob() to replace an existing trigger, or check existence first with scheduler.CheckExists() |
JobPersistenceException | Database error during job store operation | Check database connectivity, connection pool size, and query timeouts |
SchedulerException: Scheduler has been shutdown | Calling scheduler methods after Shutdown() | Ensure your application lifecycle correctly manages the scheduler |
TypeLoadException on job execution | Job class not found — possibly renamed or moved | Update JOB_CLASS_NAME in QRTZ_JOB_DETAILS (see Job Deserialization Failures) |
JobExecutionException | Unhandled exception inside IJob.Execute() | Add try-catch in your job's Execute method (see Best Practices) |
