Asynchronous Mode and Java 25
Async Configuration got defined in AsyncConfiguration.kt. Default implementation employs spring-managed task executor with virtual threads being configured in:
agent-application.properties
spring.threads.virtual.enabled=true
If spring managed task executor cannot be obtained, implementation falls back to default task executor, Executors.newCachedThreadPool().
Framework employs Asyncer abstraction consistently cross 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
- Spring Executor — Uses applicationTaskExecutor (ThreadPoolTaskExecutor), not ForkJoinPool
- Fallback Safety — Even fallback uses newCachedThreadPool(), not ForkJoinPool
- 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




