Asynchronous Mode and Java 25
Async Configuration is defined in AsyncConfiguration.kt.
Threading Model
Embabel uses a dedicated executor for all agent operations with an inheritance-based threading model.
Upgrade Note - Prior to this version, Embabel set spring.threads.virtual.enabled=true by default, forcing all applications to use virtual threads. This has been removed to avoid interfering with host application threading configuration. Embabel now inherits the threading model from spring.threads.virtual.enabled (defaults to platform threads if not set). To enable virtual threads, explicitly set spring.threads.virtual.enabled=true in your application.properties.
Configuration Properties
Two orthogonal properties control Embabel’s threading behavior:
- Threading Model Inheritance (
embabel.agent.platform.threading.override)false(default): Inherit threading model fromspring.threads.virtual.enabledtrue: Flip the threading model (virtual→platform or platform→virtual)
- Executor Sharing (
embabel.agent.platform.threading.shared)false(default): Create isolated executor for Embabel operationstrue: Share app’sapplicationTaskExecutorbean when threading models match
Configuration in agent-platform.properties:
# Flip inherited threading model (virtual-to-platform, platform-to-virtual)
embabel.agent.platform.threading.override=false
# Share app's executor when both use the same threading model
embabel.agent.platform.threading.shared=false
Behavior Matrix
| App Threads | Override | Shared | Embabel Threading Model | Executor Type |
|---|---|---|---|---|
| Platform | false | false | Platform (inherit) | Isolated platform executor (default) |
| Platform | false | true | Platform (inherit) | Shared app’s platform executor |
| Platform | true | false | Virtual (flip) | Isolated virtual executor |
| Platform | true | true | Virtual (flip) | Isolated virtual executor (models differ) |
| Virtual | false | false | Virtual (inherit) | Isolated virtual executor |
| Virtual | false | true | Virtual (inherit) | Shared app’s virtual executor |
| Virtual | true | false | Platform (flip) | Isolated platform executor |
| Virtual | true | true | Platform (flip) | Isolated platform executor (models differ) |
Key insight*: shared=true enables executor sharing when both app and Embabel use the same threading model* (either platform/platform or virtual/virtual). When threading models differ, Embabel always creates an isolated executor.
Note: Virtual threads require Java 21+. On Java 17-20, Embabel automatically falls back to platform threads even if virtual threads are requested.
Framework employs Asyncer abstraction consistently across Agent Invocation (see ), Agent Actions, Tool Loop, ShellCommands, and other scenarios.
Java 25 Implications
Prior to Java 25, the JVM was imprecise when reading container CPU limits — it often saw the host’s full CPU count rather than the container’s cgroup allocation. Java 25 corrects this to accurately respect cgroup CPU limits (e.g., Kubernetes resources.limits.cpu).
Related OpenJDK ticket: JDK-8362881
When running Embabel on Java 25 inside containers (Docker/Kubernetes), all agent actions execute serially instead of in parallel. As mentioned above this is caused by Java 25’s improved (more accurate) cgroup CPU detection, which reports availableProcessors() = 1 in CPU-constrained containers. Since ForkJoinPool.commonPool sizes its parallelism based on this value, the effective parallelism drops to 1, serializing all concurrent agent operations.
| JVM Version | availableProcessors() | Common pool parallelism | Behavior |
|---|---|---|---|
| Pre-25 | Host CPU count (e.g., 8) | 7 | Actions run in parallel |
| 25 | Container CPU limit (e.g., 1) | 1 (minimum) | Actions serialize |
Also, note: modern (post 1.3) Kotlin coroutines implementation in Dispatchers.Default and Dispatchers.IO is backed by Kotlin’s own coroutine scheduler, not Java’s ForkJoinPool.commonPool().
Why Embabel is Safe
- Asyncer Abstraction — All parallel execution routes through ExecutorAsyncer
- Dedicated Executor — Uses a dedicated executor (virtual or unbounded platform threads), not ForkJoinPool
- Unbounded Execution — Configured as unbounded to support parallel action execution
- Coroutine Defaults — Production code uses Dispatchers.IO, not Dispatchers.Default
If Users report serialization issues on Java 25, recommendation is to check for:
- Custom code using CompletableFuture.supplyAsync() without explicit executor
- Custom code using Dispatchers.Default (Kotlin coroutines)
- Spring misconfiguration — applicationTaskExecutor not properly configured
- Third-party libraries that depend on ForkJoinPool.commonPool
Workarounds (if needed)
Even though Embabel core is safe, users can apply below settings if they have custom code affected:
Option 1: Set container CPU limits
# Kubernetes
resources:
limits:
cpu: "4"
requests:
cpu: "2"
Option 2: Override ForkJoinPool parallelism
java -Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -jar agent.jar




