Version
5.0.7
Context
Vert.x Version: 5.0.0 ~ 5.0.7
JDK Version: 25
OS: Linux (Container environment)
Steps to reproduce
Describe the bug
When deploying a Verticle using .setInstances(N) combined with ThreadingModel.VIRTUAL_THREAD in Vert.x 5, all deployed instances share the exact same EventLoop thread, instead of being distributed across the EventLoopGroup in a round-robin fashion.
This causes a severe performance bottleneck where increasing the instance count does not scale I/O throughput, as all instances contend for a single thread for their I/O operations.
This behavior differs from Vert.x 4.5.x, where createVirtualThreadContext called eventLoopGroup.next() for each instance, ensuring proper distribution.
Steps to reproduce
- Configure Vert.x with a sufficient EventLoop pool size (e.g., 12).
- Deploy a simple HTTP Server Verticle with
.setInstances(10) and ThreadingModel.VIRTUAL_THREAD.
- Inspect the thread dump or log the
Thread.currentThread().getName() inside the request handler.
Result: All 10 instances report the same thread name (e.g., vert.x-eventloop-thread-0).
Note: Even if setEventLoopPoolSize(300) is set, only 1 EventLoop thread is created in the dump because Netty creates threads lazily only when they are actually used.
Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(12));
DeploymentOptions options = new DeploymentOptions()
.setInstances(10)
.setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle(() -> new AbstractVerticle() {
@Override
public void start() {
// This prints the SAME thread name for all instances
System.out.println("Instance deployed on: " + Thread.currentThread().getName());
}
}, options);
Root Cause Analysis (Code Level)
I have located the exact code causing this issue in io.vertx.core.impl.DefaultDeployment.java (lines approx. 150-161).
Inside the loop iterating over the number of instances:
// DefaultDeployment.java
case VIRTUAL_THREAD:
if (workerLoop == null) {
context = contextBuilder
.withThreadingModel(ThreadingModel.VIRTUAL_THREAD)
.build();
// The first EventLoop is captured here
workerLoop = context.nettyEventLoop();
} else {
context = contextBuilder
.withThreadingModel(ThreadingModel.VIRTUAL_THREAD)
.withEventLoop(workerLoop) // <--- BUG: The captured loop is reused for ALL subsequent instances
.build();
}
break;
- The Logic Error: The
workerLoop variable captures the EventLoop of the first instance and forces it upon all subsequent instances defined by setInstances().
- Consequence: Even if I request 300 instances, they are all bound to the single
workerLoop selected in the first iteration.
Expected behavior
Each instance created via setInstances(N) should be assigned a distinct EventLoop via eventLoopGroup.next() (Round-Robin), utilizing the full available pool size.
Workaround
Deploying verticles individually in a manual loop bypasses this issue because a new deployment context is created for each call.
// Workaround: Deploy individually
for (int i = 0; i < INSTANCES; i++) {
DeploymentOptions options = new DeploymentOptions()
.setInstances(1) // Deploy 1 at a time
.setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle(MyVerticle.class, options);
}
Do you have a reproducer?
No response
Version
5.0.7
Context
Vert.x Version: 5.0.0 ~ 5.0.7
JDK Version: 25
OS: Linux (Container environment)
Steps to reproduce
Describe the bug
When deploying a Verticle using
.setInstances(N)combined withThreadingModel.VIRTUAL_THREADin Vert.x 5, all deployed instances share the exact same EventLoop thread, instead of being distributed across the EventLoopGroup in a round-robin fashion.This causes a severe performance bottleneck where increasing the instance count does not scale I/O throughput, as all instances contend for a single thread for their I/O operations.
This behavior differs from Vert.x 4.5.x, where
createVirtualThreadContextcalledeventLoopGroup.next()for each instance, ensuring proper distribution.Steps to reproduce
.setInstances(10)andThreadingModel.VIRTUAL_THREAD.Thread.currentThread().getName()inside the request handler.Result: All 10 instances report the same thread name (e.g.,
vert.x-eventloop-thread-0).Note: Even if
setEventLoopPoolSize(300)is set, only 1 EventLoop thread is created in the dump because Netty creates threads lazily only when they are actually used.Root Cause Analysis (Code Level)
I have located the exact code causing this issue in
io.vertx.core.impl.DefaultDeployment.java(lines approx. 150-161).Inside the loop iterating over the number of instances:
workerLoopvariable captures the EventLoop of the first instance and forces it upon all subsequent instances defined bysetInstances().workerLoopselected in the first iteration.Expected behavior
Each instance created via
setInstances(N)should be assigned a distinct EventLoop viaeventLoopGroup.next()(Round-Robin), utilizing the full available pool size.Workaround
Deploying verticles individually in a manual loop bypasses this issue because a new deployment context is created for each call.
Do you have a reproducer?
No response