openzipkin/zipkin · critical · RuntimeException
Attempting to make a blocking request from an event loop. Ei
Error message
Attempting to make a blocking request from an event loop. Either use doEnqueue() or run this in a separate thread.
What it means
HttpCall.doExecute() performs a blocking join() on the Armeria response future, which would deadlock if run on an Armeria event loop. Before executing it scans httpClient.options().factory().eventLoopGroup() and, if the current thread is any event loop, throws RuntimeException telling you to use doEnqueue() (Call.enqueue) or offload to a separate thread. It is a deadlock-prevention guard, not a network failure.
Source
Thrown at zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/client/HttpCall.java:125
final String name;
final WebClient httpClient;
volatile CompletableFuture<AggregatedHttpResponse> responseFuture;
HttpCall(WebClient httpClient, RequestSupplier request, BodyConverter<V> bodyConverter,
String name) {
this.httpClient = httpClient;
this.name = name;
this.request = request;
this.bodyConverter = bodyConverter;
}
@Override protected V doExecute() throws IOException {
// TODO: testme
for (EventExecutor eventLoop : httpClient.options().factory().eventLoopGroup()) {
if (eventLoop.inEventLoop()) {
throw new RuntimeException("""
Attempting to make a blocking request from an event loop. \
Either use doEnqueue() or run this in a separate thread.\
""");
}
}
final AggregatedHttpResponse response;
try {
response = sendRequest().join();
} catch (CompletionException e) {
propagateIfFatal(e);
Exceptions.throwUnsafely(e.getCause());
return null; // Unreachable
}
return parseResponse(response, bodyConverter);
}
@SuppressWarnings("FutureReturnValueIgnored")
// TODO: errorprone wants us to check this future before returning, but what would be a sensibleView on GitHub (pinned to 878ce2a1fa)
Solutions
- Use the asynchronous API: call.enqueue(callback) instead of call.execute().
- Or offload the blocking call.execute() to a dedicated executor/thread pool so the event loop is never blocked.
- In Armeria handlers, use ServiceRequestContext.blockingTaskExecutor() (or blocking-style service) for storage calls.
Example fix
// before (inside an Armeria/event-loop handler)
List<Span> traces = storage.spanStore().getTraces(request).execute(); // RuntimeException
// after (asynchronous)
storage.spanStore().getTraces(request).enqueue(new Callback<List<List<Span>>>() {
@Override public void onSuccess(List<List<Span>> value) { /* respond here */ }
@Override public void onError(Throwable t) { /* respond 500 */ }
});
// -- or --
executor.submit(() -> storage.spanStore().getTraces(request).execute()); Defensive patterns
Strategy: fallback
Validate before calling
// detect event-loop context before any blocking execute()
boolean onEventLoop = false;
for (EventExecutor loop : webClientFactory.eventLoopGroup()) {
if (loop.inEventLoop()) { onEventLoop = true; break; }
}
if (onEventLoop) {
call.enqueue(callback); // async path
} else {
V result = call.execute(); // safe blocking path
} Type guard
static boolean shouldUseAsyncCall(WebClient client) {
for (EventExecutor loop : client.options().factory().eventLoopGroup()) {
if (loop.inEventLoop()) return true;
}
return false;
} Try / catch
// prefer structural fix (enqueue/offload) over catching; if wrapping legacy code:
try {
V v = call.execute();
} catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().contains("event loop")) {
call.enqueue(callback); // fall back to the async API
} else throw e;
} Prevention
- Default to Call.enqueue(callback) inside any async server handler; reserve execute() for worker threads.
- In Armeria services, run storage calls on blockingTaskExecutor() or use blocking task annotations.
- Add a lint/code-review rule: no .execute() on Zipkin Calls inside request-handler lambdas.
When it happens
Trigger: Calling Call.execute() on an ElasticsearchStorage SpanStore/autocomplete/health Call from inside an Armeria request handler or any callback running on the WebClient's event loop — e.g. implementing your own HTTP service that synchronously queries storage inside the handler thread.
Common situations: Building custom query endpoints on Armeria (or another async server sharing the event loop) and calling execute() in-line; migrating async code that used to run on worker threads; health-check hooks invoked from an event-loop timer.
Related errors
- no {name} property in {fileName}
- empty {name} property in {fileName}
- No valid endpoints found in ES hosts: {hosts}
- credential refresh thread didn't start
- dateSeparator must be empty or a single character
AI-assisted analysis of openzipkin/zipkin@878ce2a1fa (2026-08-14).
Data as JSON: /api/errors/06c45dbff31fbef0.
Report an issue: GitHub.