Introduction

Quartz scheduler fires triggers on schedule, but if a job takes longer than its trigger interval, the next trigger fires while the previous job is still running. This causes overlapping executions that can lead to duplicate processing, race conditions, and resource contention. Without @DisallowConcurrentExecution, Quartz runs multiple instances of the same job class simultaneously. Additionally, when the scheduler is down during a scheduled fire time, misfire handling determines whether missed executions are recovered or skipped, and incorrect misfire configuration causes either a burst of catch-up executions or permanent missed jobs.

Symptoms

bash
# Same job running twice simultaneously
2026-04-09 10:00:00 INFO  JobWorker - Starting report generation job
2026-04-09 10:00:05 INFO  JobWorker - Starting report generation job  # Duplicate!

Or misfire warnings:

bash
WARN org.quartz.impl.jdbcjobstore.JobStoreTX - Handling 15 misfired trigger(s)
# Burst of 15 jobs fired all at once after scheduler restart

Common Causes

  • No @DisallowConcurrentExecution: Job class allows concurrent instances
  • Job longer than trigger interval: Daily report takes 90 minutes, triggered hourly
  • Misfire threshold too small: Scheduler briefly down, many triggers misfire
  • Cluster mode not configured: Multiple instances fire same trigger
  • Trigger not properly deleted: Old trigger still active after reschedule
  • StatefulJob deprecated behavior: Old StatefulJob class not migrated to annotation

Step-by-Step Fix

Step 1: Prevent concurrent job execution

```java import org.quartz.DisallowConcurrentExecution; import org.quartz.PersistJobDataAfterExecution; import org.quartz.Job; import org.quartz.JobExecutionContext;

@DisallowConcurrentExecution // Only one instance at a time @PersistJobDataAfterExecution // Save JobDataMap changes public class ReportGenerationJob implements Job {

@Override public void execute(JobExecutionContext context) { // This will NOT run if a previous instance is still executing log.info("Starting report generation"); generateReports(); } } ```

Step 2: Configure misfire handling

```java // When building the trigger Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("dailyReportTrigger", "reports") .withSchedule(CronScheduleBuilder.cronSchedule("0 0 * * * ?") .withMisfireHandlingInstructionFireAndProceed()) // Default: skip missed .forJob("reportJob", "reports") .build();

// Options for misfire handling: // .withMisfireHandlingInstructionFireAndProceed() - fire once now, then resume schedule // .withMisfireHandlingInstructionDoNothing() - skip missed fire, wait for next // .withMisfireHandlingInstructionIgnoreMisfires() - fire all missed immediately ```

Step 3: Configure Quartz cluster mode

properties
# quartz.properties for clustered deployment
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.clusterCheckinInterval=15000
org.quartz.scheduler.instanceId=AUTO
org.quartz.threadPool.threadCount=10

Prevention

  • Always add @DisallowConcurrentExecution to jobs that must not overlap
  • Use CronScheduleBuilder misfire instructions appropriate for your job semantics
  • Enable Quartz clustering for high availability in multi-instance deployments
  • Monitor job execution duration and alert when approaching trigger interval
  • Use a job execution log table to track start/end times and detect overlaps
  • Configure misfire threshold (org.quartz.jobStore.misfireThreshold) based on acceptable delay
  • Test scheduler restart scenarios to verify misfire behavior is correct