Running Vert.x Handlers on Virtual Threads: A Practical Guide
Running Vert.x handlers on virtual threads lets you write blocking‑style code (e.g., JDBC) without blocking the event loop. This guide shows how to deploy a virtual‑thread verticle, use await‑style APIs, and highlights limits and mistakes to avoid.
25 Oct 2025, 05:39 UTC

Why Virtual Threads Matter for Vert.x
Vert.x’s core rule is simple: event‑loop threads must never block. In practice, that means any JDBC call, legacy SDK, or file read must be offloaded to a worker pool via executeBlocking or a reactive client. The downside is a queue, tuning knobs, and a lot of boilerplate. With Java 21’s virtual threads, Vert.x can now run a handler on a lightweight thread that parks the carrier thread when it blocks. The event loop stays free, and the code can stay sequential.
How Vert.x 5 Exposes Virtual‑Thread Verticles
Vert.x 5 introduces the ThreadingModel enum and a VirtualThreadVerticle base class. The deployment options let you choose the threading model for the whole verticle. The runtime will create a virtual thread for each logical execution path, and when that thread calls a blocking API it automatically parks the carrier thread.
Deployment Example
import io.vertx.core.Vertx;
import io.vertx.core.Future;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Verticle;
import io.vertx.core.VertxOptions;
import io.vertx.core.threading.ThreadingModel;
public class JdbcVerticle implements Verticle {
@Override
public Future<Void> start() {
// A blocking JDBC call wrapped in await‑style syntax
return Future.future(promise -> {
try {
// Assume getConnection() is a synchronous JDBC call
var conn = getConnection();
var rs = conn.createStatement().executeQuery("SELECT 1");
rs.next();
promise.complete();
} catch (Exception e) {
promise.fail(e);
}
});
}
}
// Main application
public class Main {
public static void main(String[] args) {
Vertx vertx = Vertx.vertx(new VertxOptions());
DeploymentOptions opts = new DeploymentOptions()
.setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle(JdbcVerticle.class.getName(), opts);
}
}
Key points:
ThreadingModel.VIRTUAL_THREADtells Vert.x to run each logical execution path on a virtual thread.- The
Future.await()style (orcoAwaitin Kotlin) lets you write top‑to‑bottom code without callbacks. - All blocking work inside the verticle is automatically parked on the carrier thread, so the event loop remains responsive.
When to Use Virtual Threads in Vert.x
Virtual threads are ideal for:
- Integrating legacy JDBC drivers that have no reactive client.
- Calling SDKs that are purely blocking (e.g., some cloud provider libraries).
- Reading files or performing simple I/O that cannot be made non‑blocking.
They are not a replacement for the event loop when you need high‑throughput, non‑blocking I/O (HTTP, PostgreSQL reactive client, etc.). Those paths still benefit from the event‑loop’s lightweight context switching.
Limits and Common Mistakes
- Java 21 Required: Virtual threads are only available on JDK 21+. Verify
java -versionbefore deploying. - Pinning the Carrier Thread: Calling
synchronizedblocks or native code inside a virtual thread can pin the carrier thread, defeating the benefit. Avoid heavy synchronization or useCompletableFutureinstead. - ThreadLocal‑Heavy Frameworks: Many libraries assume a small number of threads. If a library uses
ThreadLocalheavily, it can consume memory per virtual thread. Test with a small load before scaling. - CPU‑Bound Work: Virtual threads are cheap for blocking I/O, but for CPU‑bound tasks you still need to bound parallelism. Use a bounded semaphore or a dedicated worker pool.
- State Sharing: Do not share mutable state between virtual‑thread and event‑loop verticles without proper synchronization. The runtime does not provide automatic isolation.
How to Verify Your Setup
- Check Threading Model: Run the application with
-Djdk.traceVirtualThreadsand look for logs likeVirtualThread startedandCarrierThread parked. - Event Loop Responsiveness: Deploy a simple HTTP verticle that sleeps on the event loop and another that sleeps on a virtual thread. Send concurrent requests; the event‑loop one should become unresponsive while the virtual one remains fast.
- Load Test: Compare
executeBlockingvs. virtual‑thread deployment for a real JDBC query. Measure latency and throughput with a tool like JMeter or k6. - Resource Monitoring: Use
jcmd VM.native_memory summaryto ensure memory usage stays within limits when many virtual threads are active.
Rollback Considerations
Deploying a verticle with a different threading model changes the runtime behavior. If you discover performance regressions or unexpected side‑effects, you can revert by redeploying the verticle with ThreadingModel.EL (the default event‑loop) or by removing the await calls and switching back to executeBlocking. No code changes are required; only the deployment options differ.
Conclusion
Virtual threads give Vert.x developers a clean, first‑class way to write blocking‑style code while keeping the event loop free. They simplify integration with legacy JDBC drivers and other synchronous APIs, reduce boilerplate, and can improve latency when used correctly. However, they come with constraints: Java 21+, careful avoidance of carrier‑thread pinning, and awareness that CPU‑bound workloads still need bounded parallelism. By following the deployment pattern above, verifying with thread‑dump diagnostics, and conducting targeted load tests, you can safely adopt virtual threads in your Vert.x applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.