Diagnosing Vert.x 'Thread blocked' Warnings from BlockedThreadChecker
Learn how to spot Vert.x BlockedThreadChecker warnings, tell event‑loop from worker blocks, diagnose the cause, apply fixes, and know when to escalate.
27 Aug 2026, 08:43 UTC

Recognizable condition
When Vert.x’s BlockedThreadChecker detects that a thread has stayed busy longer than the configured threshold, it logs a warning similar to:
2026-10-13 06:05:12,345 WARN io.vertx.core.impl.BlockedThreadChecker - Thread Thread[vert.x-eventloop-thread-3] has been blocked for 2104 ms, time limit is 2000 ms
The warning is often accompanied by rising latency, HTTP request timeouts, or unresponsive endpoints. The blocked thread is either an event‑loop thread (named vert.x-eventloop-thread-*) or a worker thread (vert.x-worker-thread-*). Event‑loop threads must never execute blocking or CPU‑intensive work; otherwise the reactor stalls and all other handlers on that thread are delayed.
Short cause / diagnostic table
| Observed symptom | Likely cause | Where to look first |
|---|---|---|
Warning mentions vert.x-eventloop-thread-* | Blocking call (JDBC, file I/O, Thread.sleep, heavy JSON/CPU) on the event loop | Stack trace printed with the warning |
Warning mentions vert.x-worker-thread-* | Worker pool saturated or individual task exceeds maxWorkerExecuteTime | Worker pool size and maxWorkerExecuteTime settings |
| Multiple warnings with similar stack traces | Recurring blocking pattern (e.g., each request does the same DB call) | Code path in the traced method |
| Warnings correlate with GC pauses | Stop‑the‑world GC consuming the event‑loop time slice | GC logs (e.g., -Xlog:gc*) |
| Thread dumps show threads waiting on locks | Deadlock or lock contention inside user code or a library | Full thread dump (jstack/jcmd) |
Ordered checks
- Capture the warning’s stack trace – The log line includes the full stack; copy it to a file for later analysis.
- Identify the thread type – Look at the thread name in the warning:
vert.x-eventloop-thread-indicates an event‑loop block;vert.x-worker-thread-indicates a worker block. - Audit the code path – Examine the top‑most application frame in the stack trace. Determine whether it invokes a blocking API (e.g.,
java.sql.Connection,java.io.FileInputStream,Thread.sleep) or performs a heavy computation. - Review GC and pool configuration – Check the Vert.x options that were used to start the instance (
maxEventLoopExecuteTime,warningExceptionTime,workerPoolSize,maxWorkerExecuteTime). Look at GC logs for pause times that approach or exceed the warning threshold. - Reproduce under controlled load – Temporarily lower the threshold (e.g., set
maxEventLoopExecuteTime=500) and run a representative load test (usingwrk,hey, or Vert.x’s own load‑test utilities). This surfaces marginal blocks that might be hidden under the default 2‑second limit.
Fixes tied to findings
Blocking call on the event loop
If the stack trace shows a JDBC call, file operation, or Thread.sleep on an event‑loop thread, move the blocking work to the worker pool or replace it with a non‑blocking alternative.
// ❌ Bad: blocking JDBC on event loop
router.get("/api/users").handler(ctx -> {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users")) {
ResultSet rs = stmt.executeQuery();
// … process rs …
ctx.json(result);
} catch (SQLException e) {
ctx.fail(e);
}
});
✅ Fixed with vertx.executeBlocking (still uses a worker thread but keeps the event loop free):
router.get("/api/users").handler(ctx -> {
vertx.executeBlocking(future -> {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users")) {
ResultSet rs = stmt.executeQuery();
// … process rs …
future.complete(result);
} catch (SQLException e) {
future.fail(e);
}
}, res -> {
if (res.succeeded()) {
ctx.json(res.result());
} else {
ctx.fail(res.cause());
}
});
});
✅ Even better: replace JDBC with Vert.x’s non‑blocking SQL client (e.g., io.vertx.ext.sql.SQLClient) which never blocks the event loop.
CPU‑heavy computation on the event loop
If the offending frame performs a large loop, encryption, or JSON serialization, split the work into smaller chunks and schedule each chunk via vertx.setTimer or vertx.executeBlocking.
// Example: chunked processing
private void processLargeList(List<Item> items, Handler<AsyncResult<Void>> handler) {
int index = 0;
Handler<Void> chunk = v -> {
int end = Math.min(index + 100, items.size());
for (int i = index; i < end; i++) {
// process items[i]
}
index = end;
if (index < items.size()) {
vertx.setTimer(0, chunk); // yield to event loop
} else {
handler.handle(Future.succeededFuture());
}
};
vertx.setTimer(0, chunk);
}
Undersized worker pool
When the warning points to a worker thread and the stack trace shows legitimate blocking work (e.g., a third‑party SDK that cannot be made async), increase the pool size or the per‑task execution timeout.
Example Vert.x options (Java):
VertxOptions opts = new VertxOptions()
.setWorkerPoolSize(20) // default is Runtime.getRuntime().availableProcessors() * 2
.setMaxWorkerExecuteTime(30_000); // 30 seconds, adjust if tasks are known to be long
Then create the Vertx instance: Vertx vertx = Vertx.vertx(opts);
GC pauses
If GC logs show stop‑the‑world pauses comparable to the warning threshold, tune the heap size or collector (e.g., use G1GC with appropriate -XX:MaxGCPauseMillis). Reducing allocation rate in hot paths also helps.
Escalation criteria
Escalate to deeper investigation when:
- The warning persists after offloading all identifiable blocking calls to workers or async clients.
- Thread dumps reveal deadlocks, lock chains, or threads stuck in
Object.wait/Object.notifyloops originating from your code or a library. - GC pauses account for >50 % of the blocked time observed in the warnings.
- The blocked thread originates inside a third‑party library that has no async alternative and consumes a significant fraction of the worker pool.
In these cases, collect:
- A series of thread dumps (e.g.,
jstack -l <pid> > dump1.txt, repeat after 10 s). - Async profiler or Java Flight Recorder data focused on
BlockedThreadCheckercallbacks. - Dependency review to see if a newer version offers non‑blocking APIs.
Only after confirming that the root cause is not a simple mis‑placed blocking call should you consider architectural changes such as introducing a dedicated verticle type, adjusting the event‑loop count (setEventLoopPoolSize), or moving the workload to a separate service.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.