Running Blocking Code on Vert.x Worker Verticles
Move synchronous JDBC, file, or legacy client calls off the Vert.x event loop onto worker verticles to keep handlers responsive.
20 Jul 2026, 16:09 UTC

The problem: one blocking call stalls the event loop
A Vert.x verticle usually runs on an event loop, a small set of threads that dispatch non-blocking handlers. If a handler performs synchronous work—a JDBC query, file read, or legacy client call—that thread cannot process other events until the call returns. Every route sharing that loop sees higher latency, and timeouts can cascade across the application.
Useful takeaway: keep event-loop handlers non-blocking and move synchronous work to a worker verticle, which runs on a separate thread pool. Return results through a Vert.x Future or event-bus message so the rest of the application stays asynchronous.
Requirements
- Event-loop threads must stay free for network I/O and handler dispatch.
- Blocking or CPU-heavy work must run off the event loop.
- Results must flow back to the caller without shared mutable state.
- Overload must be visible, not hidden by an unbounded queue.
Smallest suitable design
Deploy two verticles. The first is a standard event-loop verticle that owns the HTTP route. The second is a worker verticle that performs the synchronous call. The event-loop verticle sends a message over the event bus and waits for a reply inside a Future.
// Event-loop verticle: stays non-blocking
router.get("/api/report").handler(rc -> {
vertx.eventBus().request("worker.generate-report", rc.getBodyAsJson(), ar -> {
if (ar.succeeded()) {
rc.json(ar.result().body());
} else {
rc.fail(500, ar.cause());
}
});
});
// Worker verticle: runs on a separate thread pool
public class ReportWorker extends AbstractVerticle {
public void start() {
vertx.eventBus().consumer("worker.generate-report", msg -> {
// synchronous JDBC or file I/O is safe here
Object result = blockingReportService.generate(msg.body());
msg.reply(result);
});
}
}
// Deployment: opt into the worker thread pool
DeploymentOptions options = new DeploymentOptions()
.setWorker(true)
.setWorkerPoolName("report-pool")
.setWorkerPoolSize(4);
vertx.deployVerticle(new ReportWorker(), options);
Run this code inside your Vert.x application. No special operating-system permissions are required beyond the application’s normal runtime privileges. Replace ReportWorker and report-pool with names that match your service. Defaults and configuration keys vary by Vert.x release, so verify the worker-pool settings against the documentation for the version you run.
Trust and data boundaries
The event-loop verticle and the worker verticle run on different threads. Do not pass mutable objects between them and assume safe sharing. Treat the worker as a separate boundary: it receives a message, performs work, and replies. If multiple workers touch the same database row or in-memory cache, coordinate with the database, locks, or the event bus—worker status alone does not make code thread-safe.
Operational checks
- Log the thread name inside the route handler and inside the worker:
log.info("Handler thread: {}", Thread.currentThread().getName());
Expected result: event-loop thread names typically contain eventloop, while worker thread names contain worker. Different thread groups confirm that work has moved off the loop.
- Confirm the deployed worker pool size matches
setWorkerPoolSize. - Watch worker queue depth under load; a growing queue means workers are slower than arrivals.
- Compare p95 request latency before and after the change with a representative load test.
Failure modes
- Worker pool exhaustion: requests queue while waiting for a free thread, raising tail latency.
- Unbounded queues: overload can hide in memory and lead to cascading failures under sustained load.
- Long worker execution: a slow call ties up a worker thread and starves other work in the same pool.
- Shared-state races: unsynchronized access to shared objects from multiple workers causes data corruption.
Conditions that would change the design
- If a reactive driver exists for the database or client, prefer it over a worker verticle.
- If strict ordering is required, use a single worker instance or serialize messages by key.
- If the latency budget is tight, measure whether the worker-hop context switch is acceptable.
- If blocking work is rare and very short, a worker pool is still safer than the event loop, but context-switch cost may matter.
Rollback
Changing deployment options changes runtime state. To roll back, redeploy the previous verticle set without the worker option or restore the earlier deployment descriptor, then restart the affected Vert.x instance. Test the rollback in staging before production.
Limitation and practical verification
Worker verticles move the thread but do not remove concurrency risk. Verify the result by logging thread names in both the route handler and the worker, and by measuring p95 latency under load before and after the change. If thread names are identical, the work is still running on the event loop.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.