Vert.x: When to Use Worker Verticles vs. Non‑Blocking APIs for Blocking Operations
Decide quickly whether to wrap blocking code in a worker verticle or refactor to a non‑blocking API. A concise table, trade‑offs, and a concrete example show how to keep your Vert.x app responsive.
01 Oct 2025, 13:09 UTC

Decision Context
Vert.x is built around a single‑threaded event loop. Any blocking call executed on that loop will pause every other handler, causing latency spikes or even application death. Two common ways to keep the event loop free when you must run blocking code are:
- Worker Verticles – code runs on a dedicated thread pool.
- Non‑Blocking APIs – rewrite the operation to use async libraries or callbacks.
Which path is right depends on the nature of the operation, the libraries you use, and the performance profile of your application.
Options in a Compact Table
| Feature | Worker Verticle | Non‑Blocking API | Notes |
|---|---|---|---|
| Threading model | Dedicated worker thread pool (size defaults to CPU count) | Single event loop thread | All code stays on the event loop |
| Legacy library support | Full – any synchronous API can be called | Limited – must find or build an async wrapper | Great for JDBC, file I/O, external SDKs |
| Throughput | Lower – limited by worker pool size and context switches | Higher – no additional threads, less context switching | Depends on operation size |
| Latency | Higher – worker thread may be queued | Lower – stays on the event loop | Critical for real‑time services |
| Complexity | Easy – just deploy as worker verticle | High – refactor code, use async libraries | Consider team skill set |
| Monitoring | Vert.x metrics expose worker pool usage and queue length | Event loop lag metrics only | Worker pool can be a bottleneck |
| Risk | Exhausting worker pool if misused; thread starvation | Blocking code on event loop if forgotten | Both can freeze the app |
Trade‑Offs
- Worker Verticles
- Pros: Zero refactoring, works with any legacy synchronous API.
- Cons: Extra threads, potential pool exhaustion, context‑switch overhead.
- Non‑Blocking APIs
- Pros: Full event‑loop scalability, lower latency, no worker pool.
- Cons: Requires async libraries, may need significant code changes.
Implementation Example: Worker Verticle
Below is a minimal Vert.x verticle that sleeps for 2 seconds inside a worker verticle. The example demonstrates that the event loop remains responsive.
public class SleepWorker extends AbstractVerticle {
@Override
public void start() {
vertx.deployVerticle(new SleepVerticle(), new DeploymentOptions().setWorker(true));
}
}
class SleepVerticle extends AbstractVerticle {
@Override
public void start() {
vertx.setPeriodic(1000, id -> {
vertx.executeBlocking(promise -> {
try {
Thread.sleep(2000); // blocking code
promise.complete("done");
} catch (InterruptedException e) {
promise.fail(e);
}
}, res -> {
if (res.succeeded()) {
System.out.println("Sleep finished: " + res.result());
}
});
});
}
}
Run it with:
java -cp vertx-core-4.x.jar:. SleepWorker
During the 2‑second sleep, other periodic handlers will still execute, proving the event loop is not blocked.
Implementation Example: Non‑Blocking API
Assume you need to query a database. Instead of the classic JDBC call, use the io.vertx.ext.jdbc.JDBCClient which provides an async API. The following snippet performs the same query without a worker verticle.
JDBCClient client = JDBCClient.createShared(vertx, new JsonObject()
.put("url", "jdbc:postgresql://localhost:5432/test")
.put("driver_class", "org.postgresql.Driver")
.put("max_pool_size", 30));
client.getConnection(ar -> {
if (ar.failed()) {
System.err.println(ar.cause());
return;
}
SQLConnection connection = ar.result();
connection.query("SELECT * FROM users", res -> {
if (res.failed()) {
System.err.println(res.cause());
} else {
System.out.println("Rows: " + res.result().getNumRows());
}
connection.close();
});
});
All database interaction stays on the event loop, and the client internally uses a pool of worker threads but those are managed by the JDBC client, not your application.
Validation & Monitoring
- Event Loop Lag – use
vertx-micrometerorvertx-metricsto exposevertx.event_loop_lag. A spike indicates blocking code on the loop. - Worker Pool Usage – metrics like
vertx.worker_pool_sizeandvertx.worker_pool_queueshow how busy the worker pool is. If the queue grows, increase the pool size withvertx.options.setWorkerPoolSize(). - Throughput Test – deploy an HTTP server that triggers the blocking operation and measure request latency with
wrkorhey. Compare the results with and without the worker verticle. - Thread Safety – remember that worker verticles run on separate threads; avoid accessing shared mutable state without proper synchronization.
Conclusion
Use a worker verticle when you must call a blocking API that has no async equivalent – it’s the quickest way to keep the event loop free. Opt for a non‑blocking API when you can refactor; it yields higher throughput and lower latency, but demands more effort and a solid async library ecosystem. Always monitor event loop lag and worker pool queue length to catch regressions early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.