← All articles
Backend1 min read

Background Workers with Spring @Async and @Scheduled

Offload slow work and run recurring jobs without blocking request threads. Here's how to use Spring's async and scheduling support correctly.

S

Swapnika Voora

Author

Not every task belongs in the request/response cycle. Sending email, generating reports, or syncing external systems should happen off the request thread. Spring gives you two lightweight tools for this: @Async and @Scheduled.

Enable the machinery

Both features are opt-in. Add the enabling annotations to a configuration class and define a dedicated thread pool so async work never starves your web threads.

AsyncConfig.java
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig {
 
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("worker-");
        executor.initialize();
        return executor;
    }
}

Fire-and-forget with @Async

Annotate a method with @Async and Spring runs it on the executor above. Return CompletableFuture when the caller needs the result later.

EmailService.java
@Service
public class EmailService {
 
    @Async("taskExecutor")
    public CompletableFuture<Void> sendWelcome(String to) {
        // slow SMTP call runs off the request thread
        mailClient.send(to, "Welcome!");
        return CompletableFuture.completedFuture(null);
    }
}

Note: @Async only works when the method is called through a Spring proxy. Calling it from within the same class bypasses the proxy and runs synchronously.

Recurring jobs with @Scheduled

Use @Scheduled for cron-like work. Prefer fixedDelay for jobs that should not overlap, and cron for wall-clock schedules.

ReportJob.java
@Component
public class ReportJob {
 
    @Scheduled(fixedDelay = 60_000)
    public void flushMetrics() {
        metrics.flush();
    }
 
    @Scheduled(cron = "0 0 2 * * *", zone = "UTC")
    public void nightlyReport() {
        reportService.generateDaily();
    }
}

Takeaways

  • Always define an explicit executor — the default is unbounded.
  • Remember @Async needs a proxied call to take effect.
  • Use fixedDelay to prevent overlapping runs, cron for clock schedules.

For heavier workloads that must survive restarts, graduate to a real queue like Kafka or RabbitMQ instead of in-process scheduling.

#java#spring-boot#background-worker#concurrency

More in Backend