Run Blocking Code in Vert.x Without Stalling the Event Loop
Blocking the Vert.x event loop with JDBC or file I/O destroys throughput. Learn when to use worker verticles and how to size the worker pool.
19 Jan 2026, 00:01 UTC

A synchronous database call can stall your whole Vert.x server
You add a JDBC query to a Vert.x HTTP handler. Under light load it works. Under load, response times climb and other unrelated endpoints slow down too. The cause is usually the Vert.x event loop: a small set of threads that run all non-blocking handlers. When your handler calls a blocking API directly on that thread, every handler sharing the loop waits. Throughput for the entire instance drops, not just the slow endpoint.
The useful takeaway: keep blocking work off the event loop. Vert.x offers worker verticles and executeBlocking for this, but they are not interchangeable, and neither fixes a poorly sized thread pool.
Event loop, worker verticle, and executeBlocking
The event loop is the Vert.x thread that runs your verticle handlers, timers, and non-blocking I/O callbacks. A worker verticle is a verticle deployed with worker: true; its methods run on a separate worker thread pool, so blocking calls such as JDBC or synchronous file I/O do not stall event-loop threads. executeBlocking runs a single blocking lambda on the worker pool and delivers the result back to an async handler, usually resuming on the event loop.
Non-blocking Vert.x APIs should still be called from event-loop threads. Workers are intended for legacy or inherently blocking libraries, not as a default execution mode for all business logic.
When to use a worker verticle versus executeBlocking
Use a worker verticle when you have a cohesive set of blocking operations that should live on worker threads for the life of the deployment. Use executeBlocking when you need to run one-off blocking code inside an otherwise non-blocking handler.
| Approach | Best for | Runs on | Result delivery |
|---|---|---|---|
| Worker verticle | Long-lived blocking service, legacy blocking library | Worker thread pool | Async handler or future, back on event loop |
| executeBlocking | Occasional blocking call inside an event-loop handler | Worker thread pool | Handler invoked after the blocking lambda finishes |
Worked example: deploy a worker verticle in Vert.x 4.x
Version assumption: Vert.x 4.x. The worker executor API and defaults can differ in Vert.x 3.x, so check your release notes before copying this configuration.
This example deploys a verticle that runs on the worker pool and prints the current thread name so you can confirm it is not an event-loop thread.
package com.example;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
public class BlockingWorkerVerticle extends AbstractVerticle {
@Override
public void start(Promise startPromise) {
// This code runs on a worker thread because the verticle is deployed with worker: true
System.out.println("Worker verticle started on thread: " + Thread.currentThread().getName());
vertx.eventBus().consumer("blocking.task", message -> {
// Simulate blocking JDBC or file I/O
String result = doBlockingWork();
message.reply(result);
});
startPromise.complete();
}
private String doBlockingWork() {
try {
Thread.sleep(500); // stand-in for a blocking call
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "done";
}
}
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(
new BlockingWorkerVerticle(),
new io.vertx.core.DeploymentOptions().setWorker(true).setWorkerPoolSize(16),
ar -> {
if (ar.succeeded()) {
System.out.println("Deployed worker verticle");
} else {
ar.cause().printStackTrace();
}
}
);
Where to run: compile and run this from your project directory using your normal Java build tool. No special operating-system permissions are required beyond running the JVM. Replace com.example.BlockingWorkerVerticle with your actual class path.
Expected check: the startup log should show a thread name such as vert.x-worker-thread-0 rather than vert.x-eventloop-thread-0. If you see an event-loop name, the worker flag was not applied.
Risk: if you call doBlockingWork directly inside an event-loop handler without a worker or executeBlocking, you block every handler on that loop. Deployment itself changes Vert.x runtime state; there is no automatic rollback if a worker verticle fails to start, so undeploy it explicitly in tests or catch deployment failures.
Configure and size the worker pool
The worker pool size is configurable in Vert.x options and defaults based on available processors. An undersized pool creates queues under load and raises latency. An oversized pool increases memory use and context-switch overhead without adding throughput.
io.vertx.core.VertxOptions options = new io.vertx.core.VertxOptions()
.setWorkerPoolSize(20);
Vertx vertx = Vertx.vertx(options);
There is no universal right number. Start with the default, measure under realistic load, and adjust only if worker threads are saturated or idle.
Limitations and how to verify the result
- Worker pool exhaustion. If all worker threads are busy, new blocking tasks queue. Latency rises even though event-loop threads appear free.
- Shared mutable state. Vert.x does not synchronize shared state between event-loop and worker code. If a worker and an event-loop thread read or write the same object, you must coordinate access yourself.
- CPU-bound work. Moving a long calculation to a worker does not make it cheap; it still consumes a finite worker thread. For CPU-heavy work, consider a dedicated executor outside Vert.x.
Practical verification: generate a thread dump while the application is under load. Confirm that worker-pool threads are active while event-loop threads remain available for non-blocking work. You can also print Thread.currentThread().getName() inside your blocking code and inside a normal HTTP handler to confirm they run on different thread groups.
Bottom line
Use worker verticles or executeBlocking for blocking libraries, keep the worker pool sized to measured load, and never assume Vert.x protects shared state across threads automatically. If your handler stays non-blocking, the event loop stays available for every other request.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.