AsyncTaskExecutor, custom executors, @EnableAsync, scheduling, thread-pool configuration, and Java 21+ virtual-thread behavior.
Imagine your API receives a request that needs to send an email, generate a report, or perform another slow operation.
If the same request thread does all the work, the user may have to wait. A task executor lets the application hand work to another thread.
Scheduling is different: instead of asking “run this in another thread now,” you ask “run this at a particular time or repeatedly.”
Run work asynchronously using an executor.
Run work at scheduled times using a scheduler.
On Java 21+, Spring Boot can use virtual-thread based executors when enabled.
If there is no Executor bean in the application context, Spring Boot auto-configures an AsyncTaskExecutor.
| Situation | Default executor |
|---|---|
| Java 21+ + virtual threads enabled | SimpleAsyncTaskExecutor using virtual threads |
| Normal setup | ThreadPoolTaskExecutor with sensible defaults |
Spring Boot documents several integrations that use the auto-configured executor unless a custom Executor changes the arrangement.
Callable controller return values.First enable asynchronous method execution:
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Then mark a method with @Async:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
@Async
public void sendEmail() {
// slow work
System.out.println("Sending email...");
}
}
@EnableAsync enables the mechanism; @Async marks the method whose execution should be handled asynchronously.By default, when you register a custom Executor bean, Spring Boot's auto-configured AsyncTaskExecutor backs off.
This is useful when you need different thread names, pool sizes, queue capacity, or separate executors for different workloads.
Spring MVC, Spring WebFlux, and Spring GraphQL require a bean named applicationTaskExecutor. MVC and WebFlux require that bean to be an AsyncTaskExecutor; Spring GraphQL does not enforce that type requirement.
Spring WebSocket and JPA can use an AsyncTaskExecutor when either a single bean of that type exists or a bean named applicationTaskExecutor exists.
@Configuration(proxyBeanMethods = false)
public class MyTaskExecutorConfiguration {
@Bean("applicationTaskExecutor")
SimpleAsyncTaskExecutor applicationTaskExecutor() {
return new SimpleAsyncTaskExecutor("app-");
}
}
applicationTaskExecutor name is useful when debugging why Spring MVC/WebFlux is not using the executor you expected.You may want one executor for normal @Async work and another for application integrations.
@Configuration(proxyBeanMethods = false)
public class MyTaskExecutorConfiguration {
@Bean("applicationTaskExecutor")
SimpleAsyncTaskExecutor applicationTaskExecutor() {
return new SimpleAsyncTaskExecutor("app-");
}
@Bean("taskExecutor")
ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("async-");
return executor;
}
}
| Bean | Typical role in this setup |
|---|---|
applicationTaskExecutor | Spring MVC/WebFlux/GraphQL and related integrations |
taskExecutor | Regular task execution such as @EnableAsync |
Spring Boot also provides builders that make it easier to create executors with behavior aligned with Boot's auto-configuration.
@Bean
ThreadPoolTaskExecutor taskExecutor(
ThreadPoolTaskExecutorBuilder builder) {
return builder.build();
}
There is also SimpleAsyncTaskExecutorBuilder for creating a SimpleAsyncTaskExecutor.
If a bean named taskExecutor is not suitable, you can define an AsyncConfigurer and return the executor that should handle regular asynchronous tasks.
@Bean
AsyncConfigurer asyncConfigurer(ExecutorService executorService) {
return new AsyncConfigurer() {
@Override
public Executor getAsyncExecutor() {
return executorService;
}
};
}
@Bean
ExecutorService executorService() {
return Executors.newCachedThreadPool();
}
AsyncConfigurer → getAsyncExecutor() tells Spring which executor to use for regular @Async execution.You can define a custom Executor while preventing it from becoming the default candidate for auto-configuration decisions.
@Bean(defaultCandidate = false)
@Qualifier("scheduledExecutorService")
ScheduledExecutorService scheduledExecutorService() {
return Executors.newSingleThreadScheduledExecutor();
}
When autowiring this custom executor elsewhere, use the matching @Qualifier.
Spring Boot provides spring.task.execution.mode=force when you want the auto-configured AsyncTaskExecutor even when a custom Executor bean exists.
spring.task.execution.mode=force
In YAML:
spring:
task:
execution:
mode: force
applicationTaskExecutor is used for the documented integrations even when a custom executor or @Primary executor is present. For regular async tasks, an AsyncConfigurer can still override the executor.When ThreadPoolTaskExecutor is auto-configured, Spring Boot uses 8 core threads by default. You can tune the pool through spring.task.execution.
spring.task.execution.pool.max-size=16
spring.task.execution.pool.queue-capacity=100
spring.task.execution.pool.keep-alive=10s
Equivalent YAML:
spring:
task:
execution:
pool:
max-size: 16
queue-capacity: 100
keep-alive: "10s"
| Property | Meaning |
|---|---|
max-size | Maximum number of threads. |
queue-capacity | Number of tasks that can wait in the queue. |
keep-alive | How long extra idle threads are kept before being reclaimed. |
A scheduler is used when a task needs to execute according to a schedule. Spring Boot can auto-configure a scheduler when scheduled task execution is needed, for example with @EnableScheduling.
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class MyApplication {
}
A scheduled method can then be declared using Spring's scheduling annotations:
@Scheduled(fixedRate = 5000)
public void refreshData() {
System.out.println("Refreshing...");
}
@Async answers “run this asynchronously”; @Scheduled answers “run this according to a schedule.”| Situation | Scheduler |
|---|---|
| Virtual threads enabled | SimpleAsyncTaskScheduler using virtual threads |
| Virtual threads not enabled | ThreadPoolTaskScheduler |
The normal ThreadPoolTaskScheduler uses one thread by default.
spring.task.scheduling.thread-name-prefix=scheduling-
spring.task.scheduling.pool.size=2
YAML:
spring:
task:
scheduling:
thread-name-prefix: "scheduling-"
pool:
size: 2
| Property | Purpose |
|---|---|
thread-name-prefix | Makes scheduler thread names easier to identify in logs. |
pool.size | Controls the scheduler pool size. |
Spring Boot supports virtual-thread-based task execution and scheduling when running Java 21+ and enabling:
spring.threads.virtual.enabled=true
Uses SimpleAsyncTaskExecutor with virtual threads.
Uses SimpleAsyncTaskScheduler with virtual threads.
The virtual-thread scheduler ignores pooling-related properties.
Spring Boot exposes builder beans when custom executors or schedulers need to be created.
| Builder | Builds |
|---|---|
ThreadPoolTaskExecutorBuilder | ThreadPoolTaskExecutor |
SimpleAsyncTaskExecutorBuilder | SimpleAsyncTaskExecutor |
ThreadPoolTaskSchedulerBuilder | ThreadPoolTaskScheduler |
SimpleAsyncTaskSchedulerBuilder | SimpleAsyncTaskScheduler |
Start with Boot's auto-configured executor.
Configure spring.task.execution.pool.* or create a custom executor.
Enable scheduling and configure spring.task.scheduling.* if needed.
Define multiple executors and make their roles explicit with names/qualifiers.
Consider virtual threads through spring.threads.virtual.enabled=true.
applicationTaskExecutor name for MVC/WebFlux/GraphQL integrations.@Async works without enabling async processing.@Qualifier when injecting a specific executor.It is Spring's asynchronous task-execution abstraction used by Spring Boot's auto-configuration for several asynchronous integrations.
By default, Spring Boot's auto-configured AsyncTaskExecutor backs off and the custom executor can become the executor used for regular task execution.
Spring MVC, WebFlux, and GraphQL look for this bean name for their task execution integration.
@Async is about asynchronous execution; @Scheduled is about executing according to a schedule.
It asks Spring Boot to auto-configure its AsyncTaskExecutor even when a custom Executor bean is present.
On Java 21+, Boot uses virtual-thread-based executor/scheduler implementations for the relevant auto-configuration.
@EnableAsync and an @Async service method.applicationTaskExecutor and print thread names.taskExecutor and compare its thread names with the application executor.max-size=16 and queue-capacity=100. Observe how the executor behaves under load.scheduling- and inspect the logs.spring.threads.virtual.enabled=true.| Need | Key API / Property |
|---|---|
| Enable async | @EnableAsync |
| Mark async method | @Async |
| Application integration executor | applicationTaskExecutor |
| Regular executor name | taskExecutor |
| Async override | AsyncConfigurer |
| Force Boot executor | spring.task.execution.mode=force |
| Execution pool settings | spring.task.execution.pool.* |
| Enable scheduling | @EnableScheduling |
| Scheduled method | @Scheduled |
| Scheduling settings | spring.task.scheduling.* |
| Virtual threads | spring.threads.virtual.enabled=true |
Spring Boot handles a lot of task execution and scheduling configuration automatically. Learn the difference between an executor and a scheduler, understand the special applicationTaskExecutor bean, know how custom executors affect auto-configuration, and remember that Java 21+ virtual threads can change which implementation Boot uses.
Official reference: Spring Boot Reference Documentation — Task Execution and Scheduling. This teaching edition intentionally explains the documented concepts in simpler language and adds learning structure, examples, exercises, and interview-focused revision.