Offload Blocking Work to a Vert.x Worker Verticle
Keep Vert.x event loops responsive by moving blocking I/O to a dedicated worker verticle pool.
18 Jul 2025, 14:02 UTC

Problem
A Vert.x event loop thread handles many concurrent connections on a small number of threads. If a handler runs blocking code—such as a synchronous database call, a large file read, or a CPU-heavy loop—that thread stops processing other events until the call returns. Under load, request latency spikes and the application can appear frozen even though CPU usage is low.
Takeaway
Move the blocking code into a Worker Verticle. Vert.x runs worker verticles on a separate thread pool, so the event loop stays free to accept new requests while the worker finishes the blocking task and sends the result back through the event bus.
Prerequisites
- A Vert.x 4.x project built with Java 11 or later.
- Identified blocking code paths in an event-loop verticle or HTTP handler.
- The
vertx-coredependency on the classpath. - Permission to rebuild and restart the application.
Deploy a Worker Verticle
- Separate the blocking logic. Create a verticle that performs the blocking work. Do not call it directly from an event-loop handler.
- Deploy with worker mode enabled. In your main deployment code, use
DeploymentOptionsto mark the verticle as a worker and size the pool. - Call the worker from the event loop. Use the event bus so the caller remains non-blocking.
- Keep messages immutable. Pass simple types such as strings, buffers, or JSON objects. Do not share mutable objects between the event loop and the worker, because they run on different threads and unsynchronized access causes race conditions.
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.FileSystem;
public class BlockingFileReader extends AbstractVerticle {
@Override
public void start() {
vertx.eventBus().consumer("file.read", message -> {
String path = (String) message.body();
FileSystem fs = vertx.fileSystem();
fs.readFile(path, ar -> {
if (ar.succeeded()) {
message.reply(ar.result());
} else {
message.fail(1, ar.cause().getMessage());
}
});
});
}
}
The file-system callback runs on the worker thread because the verticle is deployed in worker mode, so this read does not stall an event-loop thread.
import io.vertx.core.DeploymentOptions;
DeploymentOptions options = new DeploymentOptions()
.setWorker(true)
.setWorkerPoolSize(8)
.setWorkerPoolName("file-reader-pool");
vertx.deployVerticle(new BlockingFileReader(), ar -> {
if (ar.succeeded()) {
System.out.println("Worker verticle deployed: " + ar.result());
} else {
ar.cause().printStackTrace();
}
});
Run this code in the application bootstrap verticle. The placeholder new BlockingFileReader() is your compiled verticle class. Increasing the pool size consumes more threads; do not set it higher than the number of available CPU cores unless the work is I/O-bound and you have measured a benefit.
vertx.eventBus().request("file.read", "/var/data/large.log", reply -> {
if (reply.succeeded()) {
Buffer content = (Buffer) reply.result().body();
// send content to the HTTP client
} else {
// handle failure
}
});
The event loop sends the message and registers a callback. It does not wait for the file read, so other requests continue while the worker pool processes the message.
Expected Checks
- Thread names in logs show
vertx-worker-thread-for the blocking code, notvertx-eventloop-thread-. - Worker pool size in the deployment options matches the running configuration.
- Event-loop delay metrics remain flat while worker pool utilization rises during a load test.
- Event bus replies return within the expected timeout; no blocking warnings appear in logs.
Recovery Options
- If the worker pool is saturated, increase
setWorkerPoolSizein small increments and re-test. - If one worker verticle handles many different blocking tasks, split it into multiple worker verticles with separate pools.
- For rare, short blocking calls, consider
vertx.executeBlocking()instead of a dedicated worker verticle. - If a worker verticle deadlocks or throws repeatedly, undeploy it using the deployment ID returned by
deployVerticle, fix the code, and redeploy.
Limitations and Verification
Worker verticles add context-switching latency compared with pure event-loop code. Use them only for work that cannot be written asynchronously. API names such as DeploymentOptions and setWorker are stable in Vert.x 4.x, but differ in Vert.x 3.x; verify your release before copying configuration.
To verify the result, take a baseline measurement of event-loop delay with the blocking code still on the event loop. Then deploy the worker verticle, move the call to the event bus, and repeat the measurement. Event-loop delay should stay low while the worker thread pool handles the load. Check the Vert.x metrics endpoint or thread dump to confirm worker threads are active and event-loop threads are not blocked.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.