Skip to content

[Vert.x 5] Regression: setInstances(N) shares a single EventLoop for all instances when using ThreadingModel.VIRTUAL_THREAD #5924

@gusah009

Description

@gusah009

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

  1. Configure Vert.x with a sufficient EventLoop pool size (e.g., 12).
  2. Deploy a simple HTTP Server Verticle with .setInstances(10) and ThreadingModel.VIRTUAL_THREAD.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions