Java Cron Jobs: Spring, Quartz and Plain Cron

Java has no cron of its own, and the two schedulers most people reach for both read cron expressions differently from the crontab you copied them out of.

Updated 5 August 2026 · 8 min read

Java has no built-in cron, so you either run a JAR from the system crontab or schedule inside the application. Spring @Scheduled takes six fields, starting with seconds. Quartz takes six or seven, also starting with seconds, and numbers days of the week 1 to 7 with 1 as Sunday. Pasting a five-field Unix expression into either shifts every value by one position.

Key takeaways

  • Spring cron expressions have six fields. 0 3 * * * from a crontab is not valid there.
  • Quartz takes six or seven fields and requires ? in either day-of-month or day-of-week.
  • Quartz numbers day-of-week 1 to 7 with 1 as Sunday. Unix and Spring use 0 as Sunday.
  • Spring's default scheduler pool size is 1, so one slow job delays every other scheduled task.
  • Running a JAR from the system crontab needs the absolute path to java, since cron sets no JAVA_HOME.

Three ways to schedule Java work

There is no cron in the JDK. What you choose depends on whether the work belongs to a long-running application or to a standalone task.

ApproachCron syntaxBest for
System crontab running a JAR5 fields (standard Unix)Standalone batch tasks, no app server involved
Spring @Scheduled6 fields (seconds first)Work inside an application that is already running
Quartz Scheduler6 or 7 fields (seconds first)Persistence, clustering, misfire handling
ScheduledExecutorServicenone, fixed intervals onlySimple repeating work with no calendar logic

Skip java.util.Timer and TimerTask. A single uncaught exception kills the timer thread and every task on it stops silently, which is precisely the failure this article is about avoiding.

Running a JAR from the system crontab

The simplest option, and the right one for a task that does not need an application context. It also behaves like every other cron job, so the usual troubleshooting applies.

0 3 * * * /usr/bin/java -jar /opt/app/report.jar >> /var/log/report.log 2>&1

# Find the absolute path first: cron does not set JAVA_HOME
which java
readlink -f $(which java)

# With JVM options and a profile
0 3 * * * /usr/bin/java -Xmx512m -Dspring.profiles.active=batch \
  -jar /opt/app/report.jar >> /var/log/report.log 2>&1

Cron sets no JAVA_HOME and its PATH is typically only /usr/bin:/bin, so a JVM installed through SDKMAN or a distribution alternative may not be found. Hard-code the path, or set JAVA_HOME at the top of the crontab.

Have the task call System.exit(1) on failure. Cron ignores exit codes, but wrappers, supervisors and monitoring do read them, and a job that swallows its exception exits 0 and reports success.

Spring @Scheduled: six fields, not five

This is the mistake that costs the most time. Spring cron expressions carry a seconds field at the front, so a five-field expression copied from a crontab is either rejected or, worse, silently means something else.

second  minute  hour  day-of-month  month  day-of-week

"0 0 3 * * *"      // 03:00 every day
"0 */15 * * * *"   // every 15 minutes
"0 0 9 * * MON-FRI" // 09:00 on weekdays
"0 0 0 1 * *"      // midnight on the 1st

// A crontab line pasted in unchanged is wrong:
"0 3 * * *"        // rejected: only five fields
@Configuration
@EnableScheduling          // without this, nothing runs
public class SchedulingConfig { }

@Component
public class ReportTask {

    private static final Logger log = LoggerFactory.getLogger(ReportTask.class);

    @Scheduled(cron = "0 0 3 * * *", zone = "Europe/Berlin")
    public void nightlyReport() {
        log.info("nightly report started");
        try {
            // ... do the work ...
            log.info("nightly report finished");
        } catch (Exception e) {
            log.error("nightly report failed", e);
        }
    }
}

The zone attribute matters more than it looks. Without it, schedules follow the JVM default timezone, which on a container is almost always UTC even when the rest of your business runs on local time.

Since Spring 5.3 the expressions also accept macros (@daily, @hourly, @midnight) and the L and # operators for things like the last day of the month.

The single-threaded default

Spring's default task scheduler has a pool size of one. Every @Scheduled method in the application shares that single thread, so one job that takes twenty minutes delays every other scheduled task behind it. The symptom is jobs that appear to run late for no reason, and it is invisible until something slows down.

# application.properties
spring.task.scheduling.pool.size=5

One slow @Scheduled method blocks all the others by default. Raise spring.task.scheduling.pool.size on any application with more than one scheduled task.

fixedRate versus fixedDelay

For interval work rather than calendar work, the two look similar and differ in a way that matters under load. fixedRate measures from the start of the previous run, so a job that overruns its interval starts again immediately and can pile up. fixedDelay measures from the end of the previous run, so there is always a real gap. Prefer fixedDelay unless you specifically need runs pinned to a clock.

Check an expression before you ship it

The free cron expression generator decodes any standard five-field schedule into plain English and previews its next runs.

Quartz: a third dialect

Quartz is the option when you need schedules persisted to a database, coordinated across a cluster, or recovered after downtime. Its cron expressions take six or seven fields, the seventh being an optional year, and they differ from both Unix and Spring in ways that produce working expressions with the wrong meaning.

Unix cronSpringQuartz
Fields566 or 7
Starts withminutesecondsecond
Sunday is0 or 70 or 71
Both day fields as *allowedallowednot allowed, one must be ?

That ? requirement catches everyone once. Quartz treats day-of-month and day-of-week as mutually exclusive, so exactly one of them has to say "no specific value":

0 0 3 * * ?        // 03:00 every day
0 0 3 ? * MON-FRI  // 03:00 on weekdays
0 0 3 L * ?        // 03:00 on the last day of the month
0 0 3 ? * 6#3      // 03:00 on the third Friday (6 = Friday, since 1 = Sunday)
0 0 3 LW * ?       // 03:00 on the last weekday of the month

// Invalid: both day fields specified
0 0 3 * * *

Note the day numbering in that third-Friday example. Quartz starts the week at 1 for Sunday, so Friday is 6. In Unix cron and Spring, Friday is 5. An expression converted between the two without adjusting shifts every run by a day.

Misfires

Quartz is the only one of these three that has an opinion about missed runs. If the scheduler was down, the thread pool was exhausted, or the job was paused, the trigger has misfired, and its misfire instruction decides what happens: fire once immediately, fire everything that was missed, or skip to the next scheduled time. The default varies by trigger type, so set it explicitly on anything where running twice, or not at all, has consequences.

Which to use

  • System crontab plus a JAR when the task is standalone and no application needs to be running. Simplest thing that works, and it survives restarts for free.
  • Spring @Scheduled when the work belongs to an application that is already running and the schedule is straightforward.
  • Quartz when you need schedules stored in a database, a cluster where exactly one node must run each job, or defined misfire behaviour.
  • Kubernetes CronJob when the task should be its own pod, scheduled by the cluster rather than by any application.

A caution on the in-application options: if you run more than one instance of the application, every instance runs the schedule. Two replicas means every @Scheduled method fires twice, which for a nightly billing run is a real incident. Quartz with a JDBC job store solves this properly; a crontab on one host or a Kubernetes CronJob sidesteps it entirely.

Scale a Spring application to two replicas and every scheduled method starts running twice. Either move the schedule outside the application or use a scheduler that coordinates across nodes.

Knowing the job still runs

None of these schedulers tell you when work stops happening. A @Scheduled method whose thread was starved by a slower job, a Quartz trigger left paused after an incident, and a JAR whose crontab entry disappeared in a migration all produce the same output: nothing. The application log holds no error, because no error occurred. The work simply did not run.

The fix is the same regardless of scheduler. Have the job report in when it completes, and alert on the report failing to arrive. Cron job monitoring triggers on that silence, which is the only signal available when a scheduled task quietly stops.

See how Pulsetic's cron job monitoring catches this from the outside, across 15+ locations.

Frequently asked questions

  • Does Spring @Scheduled use standard cron syntax?

    Not quite. Spring expressions have six fields, beginning with seconds, so "0 0 3 * * *" means 3 a.m. daily. A five-field expression copied from a crontab is rejected, and one padded out carelessly can mean something quite different from what you intended.

  • What is the difference between Quartz and Unix cron expressions?

    Quartz uses six or seven fields starting with seconds, numbers days of the week 1 to 7 with 1 as Sunday rather than 0, and requires a "?" in either day-of-month or day-of-week because it treats them as mutually exclusive. It also adds L, W and # operators for things like the last weekday of the month.

  • Why do my Spring scheduled tasks run late?

    The default task scheduler pool size is 1, so every @Scheduled method in the application shares one thread and a single long-running job delays all the others. Set spring.task.scheduling.pool.size to something larger than 1.

  • How do I stop scheduled jobs running twice when I scale up?

    Every instance of an application runs its own schedule, so two replicas fire each @Scheduled method twice. Use Quartz with a JDBC job store so the cluster coordinates, or move the schedule out of the application into a system crontab on one host or a Kubernetes CronJob.