Intermediate Reading time: ~8 min

Scheduling and Async

@Scheduled, @Async, thread pool management, cron expressions

Scheduling and Async

Scheduling and async execution are useful only when you understand not just what runs, but when it runs, on which threads, and with what failure semantics.

1. Definíció / Definition

Mi ez? / What is it?

Spring scheduling provides time-based execution of application tasks, while async execution offloads work from the caller thread to an executor. They are related but distinct concerns: scheduling answers when work begins, async answers how it is executed.

Miért létezik? / Why does it exist?

Because many useful workloads do not belong on the synchronous request path: report generation, cleanup jobs, cache refreshes, email sending, retry workflows, or periodic integration polling.

Hol helyezkedik el? / Where does it fit?

These features sit at the boundary of application services and infrastructure. They interact with thread pools, transactions, monitoring, graceful shutdown, and cluster topology.

2. Alapfogalmak / Core Concepts

2.1 `@Scheduled`

@Scheduled declares methods that run on a time-based trigger. It requires @EnableScheduling.

Common modes:

  • fixedRate: schedule relative to start time.
  • fixedDelay: schedule relative to completion time.
  • cron: schedule by calendar expression.
Mode Measures Best fit
fixedRate start-to-start interval heartbeat, polling
fixedDelay end-to-start interval cleanup loops, variable jobs
cron calendar rule reports, cutoff jobs

2.2 Spring cron syntax

Spring cron expressions include a seconds field, which often surprises developers coming from Unix cron.

Cron field order: seconds minutes hours day-of-month month day-of-week

Example: 0 0 2 * * * means 02:00:00 every day.

That means 02:00:00 every day. Copy-pasting a five-field cron into Spring is a classic configuration error.

2.3 `@Async`

@Async runs a method through a Spring proxy on another thread. Common return types include:

  • void
  • Future<T>
  • CompletableFuture<T>

Production systems should almost always provide an explicit executor instead of relying on defaults.

2.4 Scheduling vs execution

A scheduler triggers work; an executor runs work.

Key difference:

  • TaskScheduler decides when work should start.
  • TaskExecutor decides which thread performs the work.

Relevant Spring types:

  • ThreadPoolTaskScheduler for time-based triggers.
  • ThreadPoolTaskExecutor for async task execution.

Confusing the two leads to poor tuning, starvation, or unbounded queues in the wrong place.

2.5 Async exception handling

Errors in @Async void methods do not naturally bubble back to the caller. That means you need one of two patterns:

  • AsyncUncaughtExceptionHandler for fire-and-forget async methods;
  • CompletableFuture and explicit exception composition for value-returning workflows.

2.6 One instance versus many instances

@Scheduled runs in every application instance. In a three-pod deployment, the same scheduled method runs three times unless you introduce coordination.

Common solutions:

  • ShedLock for simple distributed locking backed by a shared store.
  • Quartz for richer persistent scheduling.

3. Gyakorlati használat / Practical Usage

A standard use case is periodic cleanup: deleting expired sessions, rotating stale export files, or syncing a cache from a reference source. These are often good fits for fixedDelay, because the next run should wait until the previous execution completes.

Daily reports are a more calendar-driven use case. They belong to cron scheduling, and in clustered production systems they usually need a distributed lock. Otherwise each node generates the same report independently, which is both wasteful and potentially harmful.

Async execution is a common fit for secondary user workflows such as welcome emails, audit publication, or post-processing that should not block an HTTP request. Still, async is not a magic performance upgrade; it merely shifts work to another thread pool. If the downstream system is slow, the cost remains real somewhere in the node.

In larger systems, separate executors become essential. Bulk report generation, outbound email, and lightweight cache refreshes should not contend in a single shared pool. Thread pools express operational policy, not just implementation detail.

4. Kód példák / Code Examples

4.1 Enabling scheduling and async execution

@Configuration
@EnableScheduling
@EnableAsync
public class TaskConfiguration implements AsyncConfigurer {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(4);
        scheduler.setThreadNamePrefix("scheduler-");
        scheduler.initialize();
        return scheduler;
    }

    @Bean(name = "notificationExecutor")
    public ThreadPoolTaskExecutor notificationExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("notify-");
        executor.initialize();
        return executor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return notificationExecutor();
    }
}

4.2 Scheduled jobs

@Service
public class MaintenanceJobs {

    @Scheduled(cron = "0 0 1 * * *")
    public void generateDailySummary() {
        // run once a day at 01:00
    }

    @Scheduled(fixedDelay = 30000)
    public void refreshReferenceData() {
        // run again 30 seconds after completion
    }
}

4.3 Async email sending

@Service
public class EmailService {

    @Async("notificationExecutor")
    public CompletableFuture<Void> sendWelcomeEmail(String email) {
        callProvider(email);
        return CompletableFuture.completedFuture(null);
    }

    private void callProvider(String email) {
        // invoke remote provider
    }
}

4.4 Async exception handling

@Configuration
public class AsyncErrorHandlingConfig implements AsyncConfigurer {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, params) ->
                LoggerFactory.getLogger(method.getDeclaringClass())
                        .error("Async failure in {} with params {}", method.getName(), Arrays.toString(params), throwable);
    }
}

5. Trade-offok / Trade-offs

Benefits

  • simple scheduling for many application-local jobs;
  • cleaner request latency by moving secondary work off the caller thread;
  • readable declarative model with annotations;
  • enough power for many small and medium production use cases.

Costs

  • proxy-based behavior can surprise developers;
  • poor thread-pool sizing causes contention or hidden backlog;
  • clustered execution can duplicate jobs;
  • async execution does not replace durable queues or workflow orchestration.

Use it when the problem is local to one service and does not require strong durability or complex orchestration.

Use something else when you need persistent scheduling, strict once-only semantics, large batch orchestration, or retryable durable delivery.

6. Gyakori hibák / Common Mistakes

6.1 Self-invocation bypassing proxies

Calling an @Async method from another method in the same bean often bypasses the Spring proxy. The result is synchronous execution when developers expected async behavior.

6.2 Relying on default executors

Defaults are convenient for demos, not operational policy. Without explicit pools, teams lose control over queueing, thread naming, sizing, and shutdown behavior.

6.3 Using fixedRate for slow jobs

If execution time exceeds the interval, jobs overlap. That can create duplicate work, lock contention, and resource exhaustion.

6.4 Misunderstanding transactions across async boundaries

@Transactional context does not simply “continue in the background” when you hop threads. Async execution creates a new call path with different transactional behavior.

6.5 Forgetting cluster coordination

A scheduled billing or settlement job running on every pod is not a minor defect; it can become a business incident. Design cluster behavior explicitly.

7. Senior szintű meglátások / Senior-level Insights

The senior question is not “can Spring schedule this?” but “what execution semantics does the business require?” Is duplicate execution acceptable? Is the task idempotent? Do we need at-most-once, at-least-once, or merely best effort? Those answers determine whether @Scheduled is sufficient or whether you need distributed locks, messaging, or an external scheduler.

Treat executors as policy boundaries. A single large pool may look simple in configuration, but it collapses operational priorities. Report generation should not starve transactional notifications; low-value cache refresh should not consume capacity needed by high-value workflows.

Use async sparingly for CPU-bound work. It is excellent for decoupling request latency from slower I/O, but it does not create free compute. If all async tasks become CPU-heavy, you merely move saturation from request threads to worker threads.

Finally, background jobs need observability equal to API endpoints. Count executions, measure duration, track failures, and name threads predictably. The system is easier to operate when invisible work becomes visible telemetry.

8. Szószedet / Glossary

  • @Scheduled: Spring annotation for time-triggered execution.
  • @Async: Spring annotation for asynchronous method execution.
  • fixedRate: fixed interval between starts.
  • fixedDelay: fixed interval after completion.
  • cron: calendar-based trigger expression.
  • TaskScheduler: scheduling infrastructure.
  • TaskExecutor: async execution infrastructure.
  • CompletableFuture: composable asynchronous result container.
  • ShedLock: distributed lock helper for scheduled jobs.
  • Quartz: advanced persistent scheduler.

9. Gyorsreferencia / Cheatsheet

Topic Good default Watch out for
@Scheduled simple periodic jobs each pod runs it
fixedRate heartbeat and polling overlapping runs
fixedDelay variable-duration work schedule drift
cron calendar events Spring includes seconds
@Async I/O-heavy secondary workflows define your own executor
CompletableFuture error and result composition exception handling discipline
TaskScheduler timing control separate from execution pools
TaskExecutor async worker pool queue and thread sizing
ShedLock clustered single execution shared lock store required

🎮 Games

8 questions