quarkusio/quarkus · error · BlockingOperationNotAllowedException

Attempting a blocking write on io thread

Error message

Attempting a blocking write on io thread

What it means

awaitWriteable() blocks waiting for the Vert.x response write queue to drain. If it finds itself running on an event-loop (IO) thread, blocking would freeze the event loop, so it throws BlockingOperationNotAllowedException "Attempting a blocking write on io thread". Quarkus forbids blocking operations on IO threads.

Source

Thrown at extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxBlockingOutput.java:150

            if (res.succeeded())
                ret.complete(null);
            else
                ret.completeExceptionally(res.cause());
        };
    }

    private void awaitWriteable() throws IOException {
        if (first) {
            first = false;
            return;
        }
        assert Thread.holdsLock(request.connection());
        while (request.response().writeQueueFull()) {
            if (throwable != null) {
                throw new IOException(throwable);
            }
            if (Context.isOnEventLoopThread()) {
                throw new BlockingOperationNotAllowedException("Attempting a blocking write on io thread");
            }
            if (request.response().closed()) {
                throw new IOException("Connection has been closed");
            }
            if (!drainHandlerRegistered) {
                drainHandlerRegistered = true;
                Handler<Void> handler = new Handler<Void>() {
                    @Override
                    public void handle(Void event) {
                        if (waitingForDrain) {
                            HttpConnection connection = request.connection();
                            synchronized (connection) {
                                connection.notifyAll();
                            }
                        }
                    }
                };
                request.response().drainHandler(handler);

View on GitHub (pinned to e1c734241f)

Solutions

  1. Run the endpoint or the producing code on a worker thread: annotate the resource method with @Blocking (SmallRye) so blocking writes happen off the event loop.
  2. Use a reactive/async output path (return Uni/Multi or CompletionStage) instead of blocking writes.
  3. If you control threading, dispatch to a worker via vertx.executeBlocking or workerDispatcher before writing.
  4. Reduce payload so writes complete without filling the write queue while on the event loop (workaround, not a fix).

Example fix

// before
@GET
@Path("/file")
public File download() { ... } // blocking write on event loop

// after
import io.smallrye.common.annotation.Blocking;

@GET
@Blocking
@Path("/file")
public File download() { ... }
Defensive patterns

Strategy: validation

Validate before calling

import io.vertx.core.Context;

if (Context.isOnEventLoopThread() && willBlockOnWrite()) {
    throw new IllegalStateException("Dispatch to a worker thread (@Blocking) before blocking writes");
}

Try / catch

try {
    blockingWrite();
} catch (io.quarkus.runtime.BlockingOperationNotAllowedException e) {
    // re-dispatch to worker thread and retry
    vertx.executeBlocking(p -> { doWrite(); p.complete(); });
}

Prevention

When it happens

Trigger: A synchronous/blocking write path (VertxBlockingOutput.write / awaitWriteable) executes while Context.isOnEventLoopThread() is true — e.g. a method annotated to run on the event loop (non-blocking route or default threading) performs blocking output until writeQueueFull().

Common situations: Returning large payloads from a non-blocking JAX-RS endpoint, calling blocking output inside a Vert.x route handler or @RunOnVirtualLoop-misconfigured bean, forgetting @Blocking / @ActivateRequestContext(threading) annotations after migrating to reactive handlers.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/bf931b24a7b79bbf. Report an issue: GitHub.