Skip to content
Spring Boot sb production 4 min read

Scheduling Tasks

Many applications need work to happen on a timer rather than in response to a request: purging expired sessions every night, polling an inbox every minute, refreshing a cache every few seconds. Spring Boot’s scheduling support lets you turn any bean method into a recurring job with a single annotation — no external scheduler, no cron daemon, no quartz configuration for the common cases.

Enabling scheduling

Switch on the feature with @EnableScheduling, usually on the main class or a config class. Without it, @Scheduled annotations are silently ignored.

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {
}

@Scheduled triggers

Annotate a no-argument method on a Spring-managed bean. There are three ways to describe when it runs.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ReportJobs {

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

    // Runs every 5 seconds, measured from the START of the previous run
    @Scheduled(fixedRate = 5000)
    public void pollQueue() {
        log.info("Polling queue at {}", System.currentTimeMillis());
    }

    // Runs 10 seconds after the PREVIOUS run FINISHES (no overlap)
    @Scheduled(fixedDelay = 10_000, initialDelay = 2000)
    public void syncInventory() {
        log.info("Syncing inventory");
    }

    // Runs at 02:30 every day (second minute hour day month weekday)
    @Scheduled(cron = "0 30 2 * * *", zone = "Europe/London")
    public void nightlyReport() {
        log.info("Generating nightly report");
    }
}

Output (console):

2026-06-13T02:30:00.004  INFO  ReportJobs : Generating nightly report
2026-06-13T10:15:02.011  INFO  ReportJobs : Syncing inventory
2026-06-13T10:15:05.001  INFO  ReportJobs : Polling queue at 1781000105001
TriggerCounts fromUse when
fixedRatestart of previous runyou want a steady cadence regardless of duration
fixedDelayend of previous runruns must never overlap; spacing matters
cronwall-clock expressioncalendar schedules (daily, weekday, monthly)

Tip: With fixedRate, if a run takes longer than the interval the next run waits its turn (the default pool has one thread) — it does not run concurrently. Prefer fixedDelay whenever overlap would cause problems.

Cron expressions

Spring uses a 6-field cron format: second minute hour day-of-month month day-of-week. This differs from the classic 5-field Unix cron.

ExpressionMeaning
0 0 * * * *top of every hour
0 */15 * * * *every 15 minutes
0 0 9-17 * * MON-FRIhourly, 9am–5pm, weekdays
0 0 0 1 * *midnight on the 1st of every month
@dailymidnight every day (macro)

Externalize the expression so ops can tune it without a rebuild:

app:
  jobs:
    report-cron: "0 30 2 * * *"
@Scheduled(cron = "${app.jobs.report-cron}")
public void nightlyReport() { ... }

Note: Setting cron to the special value "-" disables a scheduled method entirely — handy for switching a job off via configuration in certain environments.

The scheduler thread pool

By default Spring uses a single-threaded scheduler, so all @Scheduled methods share one thread and a long job blocks the others. For multiple independent jobs, size the pool explicitly.

spring:
  task:
    scheduling:
      pool:
        size: 4
      thread-name-prefix: scheduling-

Or define a TaskScheduler bean for finer control:

import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Bean
TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setPoolSize(4);
    scheduler.setThreadNamePrefix("scheduling-");
    scheduler.setWaitForTasksToCompleteOnShutdown(true);
    return scheduler;
}

Conditional scheduling

Often a job should run only in certain environments — for example, the nightly cleanup should run in production but not on every developer’s laptop. Gate the whole bean with @ConditionalOnProperty or a profile.

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

@Component
@ConditionalOnProperty(name = "app.jobs.enabled", havingValue = "true")
public class ReportJobs { ... }
# only in the production profile
app:
  jobs:
    enabled: true

The distributed scheduling caveat

@Scheduled runs independently in every instance. If you deploy three replicas, a fixedRate job fires three times per interval, and a nightly report generates three times. For a job that must run exactly once across the cluster, you need coordination:

  • ShedLock (net.javacrumbs.shedlock) — a lightweight library that takes a lock in a shared store (database, Redis) so only one node executes a given run. Annotate with @SchedulerLock.
  • A dedicated distributed scheduler (Quartz with a clustered JobStore, or an external orchestrator like Kubernetes CronJob).

Warning: Never assume @Scheduled fires once in a multi-instance deployment. Audit every scheduled method and add locking (ShedLock) for any job that has side effects like sending emails, charging cards, or writing reports.

Best Practices

  • Pick fixedDelay when overlap is harmful, fixedRate for steady cadence, cron for calendar times.
  • Size the scheduler pool to the number of independent jobs; the default is one thread.
  • Externalize cron expressions and gate jobs with @ConditionalOnProperty/profiles.
  • Use ShedLock (or equivalent) for jobs that must run exactly once across instances.
  • Keep scheduled methods short or offload heavy work to an @Async executor so the scheduler thread stays free.
Last updated June 13, 2026
Was this helpful?