skip to content

What happens when a @Scheduled fixedDelay/fixedRate method throws an unchecked exception versus a cron method? How should you make scheduled jobs resilient?

level: principalimportance: should knowfreq 45%

answer

  1. repeating @Scheduled survives thrown exceptions
  2. raw scheduleAtFixedRate does NOT survive
  3. default = log + continue, then swallowed
  4. catch inside + metrics + idempotent
  5. Spring won't interrupt a hung run — add timeouts

basics

~20 s

An uncaught exception from a fixedDelay or fixedRate task is logged but the schedule keeps running — future executions still fire. Only if you use a one-shot future would it stop. Make jobs resilient by catching and handling exceptions inside the method rather than letting them propagate.

solid answer

~50 s

For a repeating @Scheduled task (fixedRate, fixedDelay, or cron), an uncaught exception in one invocation does not cancel the schedule: Spring logs it via the scheduler's error handler and the next execution fires as normal. This is often misunderstood — people fear one failure kills the job forever, which is not the case for the annotation-driven repeating tasks. However, the exception is swallowed after logging, so silent failures accumulate unless you handle them. Best practice: wrap the body in try/catch, log with context, increment failure metrics, and decide on retry/alerting explicitly; keep the method's own error handling idempotent since fixedRate catch-up and multi-instance runs can double-execute. For app-wide behavior you can register a custom ErrorHandler on the ThreadPoolTaskScheduler. Also guard against long/hung invocations, which on a small pool starve other tasks — add timeouts inside the task since Spring will not interrupt a running @Scheduled method.

code

java · 27 lines
java
@Component
public class ResilientJob {

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

    @Scheduled(fixedDelay = 60_000)
    public void run() {
        try {
            doWork();                       // idempotent, with I/O timeouts inside
        } catch (Exception e) {
            log.error("nightly sync failed", e);
            // metrics.increment("job.sync.failure"); alert if threshold
        }
    }
}

@Configuration
@EnableScheduling
class SchedCfg {
    @Bean
    ThreadPoolTaskScheduler taskScheduler() {
        var s = new ThreadPoolTaskScheduler();
        s.setPoolSize(4);
        s.setErrorHandler(t -> LoggerFactory.getLogger("sched").error("task failed", t));
        return s;
    }
}

go deeper

for a junior

Knows to wrap the body in try/catch.

for a middle

Knows repeating tasks keep running after an exception and failures are logged.

for a senior

Adds metrics, idempotency, and I/O timeouts; knows the raw-executor contrast.

for a principal

Designs full resilience: custom ErrorHandler, timeouts, cluster dedup, observability, and the @Async handler nuance.

**Default exception behavior.** When a `@Scheduled` method throws an unchecked exception, the scheduling infrastructure routes it to an `ErrorHandler`. For **repeating** triggers (fixedRate, fixedDelay, cron), the default handler **logs the error and continues** — the schedule is *not* cancelled, and the **next execution still happens**. This is the crucial, frequently-missed fact: a single failing run does not permanently disable an annotation-based recurring job. (The 'a thrown exception stops all future runs' belief comes from raw `ScheduledExecutorService.scheduleAtFixedRate`, where an uncaught exception *does* suppress subsequent executions — but Spring's `@Scheduled` wraps tasks so repeating ones survive failures.) **But failures are swallowed.** After logging, the exception is gone. If you rely on the job (e.g. it flushes data) you get **silent degradation**: the log line may be missed, no metric moves, no alert fires. So 'it keeps running' is not the same as 'it's fine'. **Making jobs resilient — the checklist:** 1. **Catch inside the method.** Own your error handling: `try { doWork(); } catch (Exception e) { log.error("job X failed", e); metrics.increment("job.x.failure"); }`. This keeps logs contextual and lets you emit metrics/alerts. 2. **Idempotency.** Because fixedRate does catch-up and multiple instances may run the same job, design each run to be safe to repeat — no double-charging, upserts over inserts, dedup keys. 3. **Timeouts / bounded work.** Spring will **not interrupt** a running `@Scheduled` method; a hung external call holds a scheduler thread indefinitely and, on a small pool, starves other jobs. Put explicit timeouts on I/O (HTTP client read timeout, statement timeout) inside the task. 4. **Custom `ErrorHandler`.** For cross-cutting handling, set one on the scheduler: `threadPoolTaskScheduler.setErrorHandler(t -> ...)`. This centralizes logging/metrics for all scheduled tasks. Note there is no built-in retry/backoff in `@Scheduled` — combine with `@Retryable` (Spring Retry) or Resilience4j if you need retry semantics, or simply let the next scheduled tick be the retry. 5. **Observability.** Give the scheduler a thread-name prefix and record last-success timestamps / durations (Micrometer `@Timed`) so a job that 'runs but always fails' is visible on dashboards, not just buried in logs. 6. **Cluster-wide correctness.** Resilience overlaps with the multi-instance concern: use ShedLock/leader election if the job must run exactly once cluster-wide; otherwise every replica runs (and fails) independently. **Async interaction.** If a scheduled method is also `@Async`, the exception is handled by the async infrastructure (`AsyncUncaughtExceptionHandler`) instead of the scheduler's `ErrorHandler`, and the return type must be void or a Future — a subtle behavior change worth flagging. **Design takeaway.** Treat 'the schedule survives exceptions' as a safety net, not a strategy. Explicit in-method handling + idempotency + timeouts + metrics is what makes scheduled jobs production-grade; relying on the default logging alone leads to jobs that quietly do nothing for weeks.

  • A junior claims 'if my @Scheduled fixedDelay job throws once, it never runs again.' Correct them.
    For Spring's repeating @Scheduled tasks the exception is logged by the scheduler's ErrorHandler and the next execution still fires — the schedule is not cancelled. The 'stops forever' behavior is true of raw ScheduledExecutorService.scheduleAtFixedRate, not @Scheduled.
  • Your scheduled job hangs on a slow HTTP call and other jobs stop running. Why, and what do you do?
    Spring does not interrupt a running @Scheduled method, so it holds a scheduler thread; on a small pool that starves other tasks. Add explicit client read/connect timeouts (and statement timeouts) inside the job, and size/isolate the pool.
  • Where do you put cross-cutting logging for all scheduled failures?
    Register a custom ErrorHandler on the ThreadPoolTaskScheduler (setErrorHandler). It centralizes handling for every scheduled task instead of duplicating try/catch, though in-method handling is still useful for context and metrics.

saying these in an interview costs you the question

  • Claiming a thrown exception permanently stops a repeating @Scheduled job
  • Assuming Spring will time out or interrupt a hung scheduled method
  • Relying solely on default logging with no metrics/alerting
  • Ignoring idempotency despite fixedRate catch-up and multi-instance runs
  • Not realizing @Async changes which exception handler applies

context