prev: [Context propagation](06-context-propagation.md) · [README](../README.md) · next: [Virtual threads and pinning](08-virtual-threads-and-pinning.md) # 7. Which executor actually runs it Two independent decisions are involved, and conflating them is the source of most of the surprise. **Decision one — does Boot create `applicationTaskExecutor`?** `TaskExecutorConfigurations` gates it on `OnExecutorCondition`, an `AnyNestedCondition` whose arms are "there is no `Executor` bean" and "`spring.task.execution.mode` is `force`". So *any* `Executor` bean of your own removes Boot's — along with its properties, its thread-name prefix, and its context-propagation decorator. **Decision two — which executor does `@Async` resolve?** `AsyncExecutionAspectSupport` asks the bean factory for a unique bean of type `TaskExecutor`. Failing that, it looks for a bean named exactly `taskExecutor`. Failing that, it falls back to a plain `SimpleAsyncTaskExecutor`. Three contexts, all captured: | context | executor beans | `@Async` ran on | |---|---|---| | stock ([output](output/executor-report.txt)) | `applicationTaskExecutor` | `task-1` | | one custom `Executor` ([output](output/executor-one-custom.txt)) | `myExecutor` | `mine-1` | | two custom `Executor`s ([output](output/executor-two-custom.txt)) | `reportsExecutor`, `emailsExecutor` | **`SimpleAsyncTaskExecutor-1`** | The third row is the one that hurts. Neither of your carefully sized two-thread pools is used. `SimpleAsyncTaskExecutor` starts **a brand new platform thread for every single call** and has no bound, so an application that was throttled to two concurrent report generations is now unthrottled, and the symptom is thread exhaustion under load rather than anything at the point of the change. It is not entirely silent — the interceptor logs it, once, at `INFO`: ``` o.s.a.i.AnnotationAsyncExecutionInterceptor : More than one TaskExecutor bean found within the context, and none is named 'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly as an alias) in order to use it for async processing: [reportsExecutor, emailsExecutor] ``` `INFO`, on first use, in the middle of startup noise. In practice nobody sees it. ## The three ways out - **`@Async("reportsExecutor")`** — name the executor at each call site. Explicit, and it survives someone adding a third executor later. This is the right answer when the executors genuinely differ in purpose. - **`spring.task.execution.mode=force`** — Boot creates `applicationTaskExecutor` alongside yours, and `@Async` resolves it ([output](output/executor-force-mode.txt)). Use it when your extra `Executor` beans exist for something other than `@Async` and you did not mean to disturb it. - **Name one of them `taskExecutor`**, or mark it `@Primary`. Works, and reads like an accident to the next person. ## The diagnostic `ExecutorDiagnostics` prints every `Executor` bean, its class, and the real pool numbers off the live object. Two lines of it answer questions that otherwise take an afternoon: ``` Bean named 'taskExecutor' present: false Bean named 'applicationTaskExecutor' present: false ``` Delete it before shipping. It is a diagnostic, not a feature. next: [Virtual threads and pinning](08-virtual-threads-and-pinning.md)