Reactive Routes and the Vert.x HTTP Layer

This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against.

Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change.

This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every HTTP-facing piece of Quarkus — Quarkus REST, reactive routes, WebSockets Next — is a client of the same underlying Vert.x HttpServer and the same small pool of event-loop threads. There is no independent "REST stack" and "reactive routes stack" running side by side; there is one Vert.x HTTP layer, and each of those programming models is a different set of ergonomics laid on top of it. This page covers the lowest-level of those models — reactive routes, direct Vertx/WebClient/EventBus access — and the HTTP-server-level configuration (the management interface) that sits underneath all of them.

The Vert.x layer underneath everything

Reactive routes: the lowest-level HTTP model

Reactive routes let a CDI bean method handle an HTTP route directly, with almost nothing between the incoming request and your code — no JAX-RS resource class, no annotation-processing-heavy request pipeline, just a @Route-annotated method invoked by Vert.x itself. Add the capability (it ships as part of the core quarkus-vertx-http layer that Quarkus REST and every other HTTP extension already pull in, so most projects that already have a REST extension need nothing extra; the explicit artifact exists for a project with no other HTTP extension yet):

./mvnw quarkus:add-extension -Dextensions=reactive-routes

which adds:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-routes</artifactId>
</dependency>

A route is a plain CDI bean method annotated with @Route:

package com.example.routes;

import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.Route.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class PingRoute {

    @Route(path = "/ping", methods = HttpMethod.GET, produces = "text/plain")
    void ping(RoutingContext context) {
        context.response().end("pong");
    }
}

@Route accepts the pieces you would expect from a low-level HTTP mapping:

Attribute Meaning

path / regex

The path this route matches — either a concrete/parameterized path (/items/:id) or a full regular expression for cases a path template cannot express.

methods

One or more Route.HttpMethod values (GET, POST, …​); a route with no methods matches every method.

produces / consumes

Media-type matching against the request’s Accept/Content-Type headers, the same content-negotiation idea JAX-RS uses, but applied at the Vert.x routing level before any REST machinery is involved.

order

An explicit integer controlling the position of this route in Vert.x’s route-matching chain relative to other routes — lower values are tried first; matters once multiple routes or filters could match the same request.

A route method’s signature is intentionally flexible: it can take a RoutingContext, return void and write to the response itself (as above), or return a value — including a Mutiny Uni/Multi — that Quarkus serializes for you:

package com.example.routes;

import io.quarkus.vertx.web.Route;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class GreetingRoute {

    @Route(path = "/hello/:name", methods = Route.HttpMethod.GET, produces = "application/json")
    Uni<String> hello(io.vertx.ext.web.RoutingContext context) {
        String name = context.pathParam("name");
        return Uni.createFrom().item("Hello, " + name + "!");
    }
}

See Reactive Programming and Mutiny for Uni/Multi fundamentals — they behave identically here as they do returned from a Quarkus REST resource method.

@RouteFilter: cross-cutting logic below JAX-RS

@RouteFilter registers a method that runs for every matching request, before (or, with a negative priority convention reversed, after) the routes it applies to — the reactive-routes equivalent of a JAX-RS ContainerRequestFilter, but running at the raw Vert.x level, ahead of any REST-specific processing, and applying uniformly to reactive routes, Quarkus REST endpoints, and any other Vert.x Web route registered in the application:

package com.example.routes;

import io.quarkus.vertx.web.RouteFilter;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class RequestTimingFilter {

    @RouteFilter(100)
    void addTimingHeader(RoutingContext context) {
        long start = System.nanoTime();
        context.addHeadersEndHandler(void_ -> {
            long elapsedMicros = (System.nanoTime() - start) / 1_000;
            context.response().putHeader("X-Response-Time-Micros", String.valueOf(elapsedMicros));
        });
        context.next();
    }
}

The integer argument to @RouteFilter is the same kind of ordering value as @Route’s `order — lower runs earlier. Because a filter method receives the raw RoutingContext, it can inspect or rewrite headers, short- circuit the request entirely (by calling context.response().end(…​) instead of context.next()), or attach completion handlers — full control, at the cost of losing the JAX-RS-level conveniences (@Context-injected JAX-RS types, JAX-RS exception mapping) a ContainerRequestFilter would have.

Direct RoutingContext access

Both @Route methods and @RouteFilter methods work directly against io.vertx.ext.web.RoutingContext — the same object Vert.x Web itself uses — giving unmediated access to the raw request (context.request(), headers, the request body as a Buffer) and response (context.response(), streaming writes, trailers) without any JSON-binding or JAX-RS-annotation layer standing in between. This is the level to reach for whenever that mediation is itself the obstacle.

When to reach for reactive routes instead of Quarkus REST

Quarkus REST remains the right default for JSON/REST APIs — see Quarkus REST Services for that whole model, including its own approach to reactive return types, content negotiation, and validation. Reach for reactive routes instead when one of these actually applies:

  • Very low-level control over the raw request/response — streaming a response body incrementally, reading headers or the body in a shape JAX-RS annotations do not model well, or building a protocol-agnostic gateway in front of several backends.

  • A non-JSON, non-REST protocol riding over HTTP — webhooks with bespoke framing, server-sent events assembled by hand, a custom binary-over-HTTP protocol — where JAX-RS’s resource-method/media-type model does not map cleanly onto what is actually being sent.

  • Extremely performance-sensitive paths that want to skip JAX-RS processing overhead — a route method is invoked essentially directly by Vert.x, with none of the resource-matching, parameter-injection, or interceptor-chain machinery JAX-RS runs on every request; worth the extra manual wiring only where that overhead is measured to matter.

  • Framework or extension authoring — building tooling that needs to sit below any particular request model (health checks, metrics endpoints, and Quarkus’s own Dev UI are themselves implemented this way).

Outside those cases, Quarkus REST’s higher-level model is almost always the better fit — less code, standard JAX-RS semantics, and better interoperability with the rest of the Jakarta EE ecosystem.

Diagram of the Vert.x event loop and worker thread pool underneath Quarkus: an incoming HTTP request arrives at a small fixed pool of event-loop threads sized to the number of CPU cores; non-blocking work — Quarkus REST and reactive routes returning Uni/Multi

Accessing the Vert.x API directly

Reactive routes are one way to sit close to Vert.x; the extensions below let any CDI bean — a route, a Quarkus REST resource, a plain service — reach the underlying Vert.x APIs directly. Add quarkus-vertx for this:

./mvnw quarkus:add-extension -Dextensions=vertx
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx</artifactId>
</dependency>

Injecting the core Vertx instance

Quarkus manages exactly one io.vertx.core.Vertx instance per application and makes it injectable anywhere:

package com.example.vertx;

import io.vertx.core.Vertx;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class VertxAwareService {

    @Inject
    Vertx vertx;

    void scheduleCleanup() {
        vertx.setTimer(5_000, id -> System.out.println("Running delayed cleanup"));
    }
}

Everything else in this section — WebClient, the EventBus — is reached either from this same Vertx instance or injected directly, and both approaches end up sharing the identical underlying event loop that reactive routes and Quarkus REST run on.

WebClient: outbound HTTP at the Vert.x level

The MicroProfile REST Client remains the right default for calling a well-known, typed remote API — see the REST Client page for that model. The Vert.x WebClient is the lower-level alternative: useful when the target URL, method, or payload shape is dynamic, when the call needs to happen from code that has no interface to declare a REST Client against, or when building a generic HTTP proxy/gateway. Inject it directly:

package com.example.vertx;

import io.smallrye.mutiny.Uni;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.HttpResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class DynamicHttpCaller {

    @Inject
    WebClient webClient;

    Uni<String> fetch(String host, int port, String path) {
        return webClient.get(port, host, path)
                .send()
                .onItem().transform(HttpResponse::bodyAsString);
    }

    Uni<HttpResponse<Buffer>> postJson(String host, int port, String path, Buffer jsonBody) {
        return webClient.post(port, host, path)
                .putHeader("Content-Type", "application/json")
                .sendBuffer(jsonBody);
    }
}

quarkus-vertx already brings io.vertx.ext.web.client.WebClient transitively via the Vert.x Web Client libraries; no separate artifact is needed beyond the extension added above. Because WebClient calls return Mutiny types when used through Quarkus’s Vert.x integration, they compose with the same operators covered in Reactive Programming and Mutiny.

EventBus: in-process messaging

The Vert.x EventBus is an in-JVM publish/subscribe and point-to-point message bus, reachable from any bean:

package com.example.vertx;

import io.vertx.mutiny.core.eventbus.EventBus;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class OrderEventPublisher {

    @Inject
    EventBus eventBus;

    void orderPlaced(String orderId) {
        // fire-and-forget publish to every consumer of this address
        eventBus.publisher(String.class).send("orders.placed", orderId);
    }

    io.smallrye.mutiny.Uni<String> requestReplyExample(String orderId) {
        // request/reply: wait for exactly one consumer's reply
        return eventBus.<String>request("orders.validate", orderId)
                .onItem().transform(io.vertx.mutiny.core.eventbus.Message::body);
    }
}

@ConsumeEvent declares the other side — a plain bean method invoked whenever a message lands on an address, with no manual bus lookup required:

package com.example.vertx;

import io.quarkus.vertx.ConsumeEvent;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class OrderValidationConsumer {

    @ConsumeEvent("orders.placed")
    void onOrderPlaced(String orderId) {
        System.out.println("Order placed: " + orderId);
    }

    @ConsumeEvent("orders.validate")
    String onValidate(String orderId) {
        // the returned value becomes the reply for a request/reply call
        return orderId.startsWith("ORD-") ? "valid" : "invalid";
    }
}

By default a @ConsumeEvent method runs on an event-loop thread if the enclosing bean is not annotated @Blocking, matching the split covered in the diagram above; add @Blocking on the consumer method (or the bean) to have Quarkus dispatch it to the worker thread pool instead, exactly as it would a blocking Quarkus REST resource method.

The Vert.x event bus in Quarkus is local and in-JVM by default — messages never leave the process. Clustering the event bus so multiple application instances share one bus requires explicitly configuring a clustered Vert.x setup, and is uncommon in typical Quarkus deployments: most projects that need cross-instance messaging reach for a real broker instead — see Messaging for Kafka/AMQP via reactive messaging — rather than clustering the event bus itself. Treat clustering as an advanced, explicit opt-in, not a default scaling path for @ConsumeEvent.

The HTTP vs. management interface split

By default, every HTTP-facing concern — the application’s own REST/reactive-route endpoints, and Quarkus’s built-in operational endpoints (health checks, metrics, the Dev UI in dev mode) — share the same Vert.x HttpServer and port. Quarkus can instead expose the operational endpoints on a separate, dedicated management interface: its own host/port, with its own HttpServer, isolated from the public application port entirely. This is the recommended production pattern, since it means a load balancer, firewall rule, or network policy can expose the application port publicly while keeping the management port reachable only internally.

Enable it and give it its own port:

quarkus.management.enabled=true
quarkus.management.port=9000
quarkus.management.host=0.0.0.0

Once quarkus.management.enabled=true, the built-in endpoints that support the management interface move off the main application port onto this one automatically — most notably:

Endpoint Behavior once the management interface is enabled

SmallRye Health (/q/health, /q/health/live, /q/health/ready)

Served from the management interface instead of the application port.

Metrics (/q/metrics)

Served from the management interface instead of the application port.

Dev UI (/q/dev, dev mode only)

Remains reachable during dev mode; check the Management Interface Reference guide for the exact current behavior against the Quarkus version in use, since this is the one endpoint most likely to have version-specific nuances.

A reactive route or @RouteFilter can itself be bound to the management interface rather than the main application port, by setting its own management-specific configuration — useful for an internal-only diagnostic route that should never be reachable from the public port at all. Consult the Management Interface Reference guide linked below for the exact property names, since they are the kind of detail most likely to shift between minor Quarkus releases.