You’ve set up a @Scheduled method, the application starts without errors, and… nothing happens. No log output, no side effects, just silence. Spring scheduling failures are maddening precisely because they’re so quiet — the framework swallows exceptions and missing configuration without complaint.

TLDR: The most common causes are (1) missing @EnableScheduling, (2) the bean not being managed by Spring, (3) an unhandled exception silently killing the scheduler, and (4) a bad cron expression. Start there.


How to Diagnose Which Cause You Have

Before diving into fixes, add this to your application.properties:

logging.level.org.springframework.scheduling=DEBUG
logging.level.org.springframework.context.annotation=DEBUG

Also enable Spring Boot’s actuator endpoint if you have it available — /actuator/scheduledtasks lists every registered scheduled task. If your task doesn’t appear there, the issue is registration. If it appears but doesn’t fire, the issue is execution.


Cause #1: Missing @EnableScheduling

This is the most common cause by far. Spring scheduling isn’t active unless you explicitly enable it.

Symptom: Your @Scheduled method is defined, but the debug logs show no mention of it being registered.

The fix: Add @EnableScheduling to any @Configuration class — typically your main application class:

// ❌ Before: scheduling silently disabled
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
// ✅ After: scheduling explicitly enabled
@SpringBootApplication
@EnableScheduling
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

You can also put @EnableScheduling on a separate configuration class:

@Configuration
@EnableScheduling
public class SchedulingConfig {
    // optionally configure a custom task executor here
}

Either approach works. The key is that exactly one @Configuration bean in your application context must carry @EnableScheduling.


Cause #2: The Scheduled Bean Isn’t Managed by Spring

@Scheduled only works on Spring-managed beans. If you instantiate the class yourself with new, the annotation is completely ignored.

Symptom: The task shows up in /actuator/scheduledtasks in one environment but not another, or doesn’t appear at all despite having @EnableScheduling.

The broken pattern:

// ❌ This won't work — Spring doesn't manage this instance
public class ReportService {

    @Scheduled(fixedRate = 5000)
    public void generateReport() {
        System.out.println("Generating report...");
    }
}

// Somewhere else:
ReportService service = new ReportService();  // Spring has no idea this exists

The fix — let Spring create the bean:

// ✅ Spring manages this bean, @Scheduled works
@Service
public class ReportService {

    @Scheduled(fixedRate = 5000)
    public void generateReport() {
        System.out.println("Generating report...");
    }
}

Also watch for this gotcha: if you declare @Scheduled on a method in a class that’s inside a @Configuration class as a non-bean inner class, it won’t be picked up. Keep your scheduled tasks in properly annotated @Component, @Service, or @Repository classes.


Cause #3: An Exception Is Silently Killing the Scheduler

This is the most insidious cause. Spring’s default ThreadPoolTaskScheduler catches exceptions from scheduled tasks and logs them at ERROR level — but crucially, it doesn’t stop the scheduler. Except… it does stop that particular task’s fixed-delay chain if the exception propagates up.

Actually the behavior depends on which scheduler type and settings you’re using. With fixedDelay, the next execution is scheduled after the current one completes (or fails). If an exception occurs, the next execution still runs by default with the default scheduler. But if you’ve customized the scheduler or the error handler, behavior can differ.

The real problem is that developers miss the exception in logs because the app is still running fine otherwise.

What to look for — search your logs for lines like:

ERROR o.s.s.s.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task
java.lang.NullPointerException: Cannot invoke method process() on null object
    at com.example.DataSyncTask.run(DataSyncTask.java:42)

The fix — add proper error handling inside your scheduled method:

// ❌ Before: exception kills visibility into failures
@Scheduled(fixedRate = 60000)
public void syncData() {
    DataSource source = getDataSource();  // could return null
    source.process();  // NullPointerException here goes quietly to logs
}
// ✅ After: exceptions are caught and handled explicitly
@Scheduled(fixedRate = 60000)
public void syncData() {
    try {
        DataSource source = getDataSource();
        if (source == null) {
            log.warn("DataSource not available, skipping sync");
            return;
        }
        source.process();
    } catch (Exception e) {
        log.error("Data sync failed: {}", e.getMessage(), e);
        // optionally: metrics.incrementFailureCounter("data_sync");
    }
}

For a global approach, implement a custom ErrorHandler on your scheduler:

@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("my-scheduler-");
        scheduler.setErrorHandler(t -> {
            log.error("Scheduled task threw an exception: {}", t.getMessage(), t);
            // alert, metric, etc.
        });
        scheduler.initialize();
        taskRegistrar.setTaskScheduler(scheduler);
    }
}

This gives you a single place to handle all scheduling errors across your application.


Cause #4: Invalid Cron Expression

Spring uses a six-field cron format (seconds included), not the standard five-field Unix cron format. This trips up almost every developer at least once.

Spring cron format: second minute hour day-of-month month day-of-week

Unix cron format: minute hour day-of-month month day-of-week

Common mistake:

// ❌ This is a Unix-style cron expression — Spring will reject or misinterpret it
@Scheduled(cron = "0 9 * * MON-FRI")  // missing 'seconds' field

// ✅ Spring cron with all 6 fields
@Scheduled(cron = "0 0 9 * * MON-FRI")  // runs at 9:00:00 AM, Mon-Fri

Other common cron expression mistakes:

// ❌ Wrong: "every minute" in Unix cron
@Scheduled(cron = "* * * * *")

// ✅ Correct: "every minute" in Spring cron
@Scheduled(cron = "0 * * * * *")

// ❌ Wrong: day-of-week as number (0=Sunday in Unix, but Spring uses 1=Sunday)
@Scheduled(cron = "0 0 8 * * 1")  // could run Saturday or Sunday depending on intent

// ✅ Clear: use three-letter abbreviations
@Scheduled(cron = "0 0 8 * * MON")  // unambiguously Monday

Use the Spring Framework cron expression documentation or a cron validator that supports 6-field Spring format. Standard online validators often use 5-field format and will mislead you.

Also handy — Spring supports some cron macros:

@Scheduled(cron = "@hourly")    // equivalent to "0 0 * * * *"
@Scheduled(cron = "@daily")     // equivalent to "0 0 0 * * *"
@Scheduled(cron = "@weekly")    // equivalent to "0 0 0 * * 0"
@Scheduled(cron = "@monthly")   // equivalent to "0 0 0 1 * *"

Cause #5: Thread Pool Exhaustion

Spring’s default scheduler uses a single-threaded executor. If one of your scheduled tasks blocks (waiting on I/O, a slow database query, a stuck HTTP call), every other scheduled task queues up behind it.

Symptom: Tasks run fine initially, then start running late or stop entirely. Logs show tasks backing up.

Diagnosis: Add this to see how many threads your scheduler is using:

@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);  // increase from default of 1
        scheduler.setThreadNamePrefix("scheduled-task-");
        scheduler.initialize();
        taskRegistrar.setTaskScheduler(scheduler);
    }
}

Or via application.properties in Spring Boot:

spring.task.scheduling.pool.size=10
spring.task.scheduling.thread-name-prefix=scheduled-

Best practice: Don’t do slow work directly in the scheduled method. Delegate to an async executor:

@Service
public class ReportScheduler {

    private final ReportService reportService;

    public ReportScheduler(ReportService reportService) {
        this.reportService = reportService;
    }

    @Scheduled(fixedRate = 30000)
    public void triggerReport() {
        // this returns immediately, actual work happens in a separate thread
        reportService.generateAsync();
    }
}

@Service
public class ReportService {

    @Async
    public CompletableFuture<Void> generateAsync() {
        // slow work here
        return CompletableFuture.completedFuture(null);
    }
}

Remember: if you use @Async inside your scheduled methods, you need @EnableAsync in addition to @EnableScheduling.


Cause #6: Wrong Timezone in Cron

Your cron task fires at unexpected times — or seems to fire at midnight UTC when you expected midnight local time.

Spring’s @Scheduled cron runs in the JVM’s default timezone unless you specify otherwise. In cloud deployments, the JVM timezone is often UTC regardless of where your users are.

// ❌ Ambiguous — runs at midnight in whatever timezone the JVM is set to
@Scheduled(cron = "0 0 0 * * *")

// ✅ Explicit timezone — always runs at midnight New York time
@Scheduled(cron = "0 0 0 * * *", zone = "America/New_York")

For properties-driven configuration:

@Scheduled(cron = "${reports.schedule.cron}", zone = "${reports.schedule.timezone:UTC}")
public void generateReport() {
    // ...
}

And in application.properties:

reports.schedule.cron=0 0 8 * * MON-FRI
reports.schedule.timezone=America/Chicago

This makes timezone intent explicit and environment-configurable without code changes.


Cause #7: @Scheduled on a @Transactional Method in the Same Bean

This is a subtle Spring proxy issue. If you put both @Scheduled and @Transactional on the same method, and the scheduling infrastructure calls the method through Spring’s proxy, the transactional advice may not apply correctly depending on your proxy configuration.

More commonly, developers put @Transactional on a scheduled method expecting Spring to manage a transaction, but then call a @Transactional private method internally — which bypasses the proxy entirely.

The fix: Separate concerns. Keep @Scheduled on a thin scheduler class that delegates to a @Service method which carries @Transactional:

// ❌ Mixed concerns: scheduling + transaction in one class
@Component
public class OrderCleanupTask {

    @Autowired
    private OrderRepository orderRepository;

    @Scheduled(cron = "0 0 2 * * *")
    @Transactional
    public void cleanupExpiredOrders() {
        orderRepository.deleteExpiredBefore(LocalDate.now().minusDays(30));
    }
}
// ✅ Separated concerns
@Component
public class OrderCleanupTask {

    private final OrderCleanupService cleanupService;

    public OrderCleanupTask(OrderCleanupService cleanupService) {
        this.cleanupService = cleanupService;
    }

    @Scheduled(cron = "0 0 2 * * *")
    public void cleanupExpiredOrders() {
        cleanupService.deleteExpiredOrders();
    }
}

@Service
public class OrderCleanupService {

    private final OrderRepository orderRepository;

    public OrderCleanupService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void deleteExpiredOrders() {
        orderRepository.deleteExpiredBefore(LocalDate.now().minusDays(30));
    }
}

This pattern also makes both classes independently testable, which is a nice side benefit.


Still Not Working? Less Common Causes

Conditional bean creation: If your scheduler bean is conditionally created (@ConditionalOnProperty, @Profile, etc.) and the condition doesn’t match, the bean won’t exist and the task won’t run.

// Check that this property is actually set in your environment
@Component
@ConditionalOnProperty(name = "feature.scheduling.enabled", havingValue = "true")
public class MyScheduledTasks {
    // ...
}

Application context not fully started: If the task fires before all beans are initialized — rare with @Scheduled, since the scheduler starts after context refresh — it can cause NPEs. Prefer @EventListener(ApplicationReadyEvent.class) for any one-time startup task:

@EventListener(ApplicationReadyEvent.class)
public void onStartup() {
    // runs once, after all beans are ready
    initializeCache();
}

Multiple application contexts: In Spring MVC apps (not Spring Boot), you might have a parent context (loaded by ContextLoaderListener) and a child context (loaded by DispatcherServlet). @EnableScheduling in the child context only picks up beans in that context. If your scheduled bean is in the parent context, move @EnableScheduling there.

Checking registered tasks at runtime: Use the actuator endpoint if you have Spring Boot Actuator:

GET /actuator/scheduledtasks

This returns a JSON list of all fixed-rate, fixed-delay, and cron tasks that Spring has registered. If your task isn’t there, it was never registered — go back to causes #1 and #2.


Summary Checklist

Run through this list when your @Scheduled task isn’t firing:

  • [ ] @EnableScheduling is present on a @Configuration class
  • [ ] The class containing @Scheduled is annotated with @Component, @Service, etc.
  • [ ] No exception is being swallowed — check ERROR logs for TaskUtils$LoggingErrorHandler
  • [ ] Cron expression uses 6 fields (second minute hour day month weekday)
  • [ ] Timezone is explicitly set if your deployment timezone differs from expected
  • [ ] Thread pool has enough threads for concurrent tasks (spring.task.scheduling.pool.size)
  • [ ] No conditional annotations (@ConditionalOnProperty, @Profile) blocking bean creation
  • [ ] /actuator/scheduledtasks shows the task as registered

For more complex scheduler debugging — especially when tasks run but produce wrong results — use Debugly’s stack trace formatter to parse and analyze the Java stack traces from your scheduler’s error logs. Pasting a multi-cause exception from a scheduled task into a raw log makes it very hard to find the root cause; Debugly formats and highlights the relevant frames instantly.

If you’re also hitting Spring’s dependency injection issues in your scheduled beans, the Spring BeanCreationException guide covers the full range of wiring failures that can affect any Spring-managed bean, including scheduled task classes.


Scheduled task bugs are frustrating because the app gives you almost no signal when something goes wrong. Adding debug logging, checking the actuator endpoint, and wiring up a global error handler will surface 95% of issues immediately. The remaining 5% — proxy subtleties, context ordering, conditional beans — just require methodical elimination of the causes above.