WebSockets Next

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.

WebSockets Next is the modern, Vert.x-based WebSocket implementation shipped as the quarkus-websockets-next extension. It replaces the older, Undertow-based quarkus-websockets extension with a CDI-first programming model: a single annotated bean class per endpoint, method-level lifecycle callbacks instead of an Endpoint/Listener subclass, and an injectable connection object for sending messages back. This page covers that model end to end — endpoints, connections, broadcasting, security, per-connection state, back-pressure, the client-side API, and testing — plus a short note on when the legacy extension still shows up.

Installing the extension

./mvnw quarkus:add-extension -Dextensions=websockets-next

This adds the following dependency to pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-websockets-next</artifactId>
</dependency>

The @WebSocket endpoint programming model

An endpoint is a CDI bean annotated @WebSocket, with a path attribute that mirrors the way JAX-RS/Quarkus REST resources declare a @Path:

package org.acme.chat;

import io.quarkus.websockets.next.OnBinaryMessage;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.inject.Inject;

@WebSocket(path = "/chat/{room}")
public class ChatSocket {

    @Inject
    WebSocketConnection connection;

    @OnOpen
    public void onOpen() {
        // called once, right after the HTTP upgrade handshake completes
    }

    @OnTextMessage
    public void onTextMessage(String message) {
        // called once per incoming text frame
    }

    @OnBinaryMessage
    public void onBinaryMessage(io.vertx.core.buffer.Buffer message) {
        // called once per incoming binary frame
    }

    @OnClose
    public void onClose() {
        // called once the connection is closed, by either side
    }

    @OnError
    public void onError(Throwable error) {
        // called for an exception thrown from another callback, or a transport-level failure
    }
}

Quarkus creates one bean instance of ChatSocket per connection, and calls exactly one of these lifecycle methods at a time for that instance — there is no need for a request-scoped context the way a REST resource would need one, because the endpoint bean itself already scopes to the connection. This has a direct consequence covered in more detail below: instance fields on the bean are safe to use as per-connection state without extra synchronization against other connections.

The connection API: WebSocketConnection

WebSocketConnection is injectable into an endpoint bean (as above) or into any callback method as a parameter, and is the handle used to send data back, broadcast, inspect the connection, or close it:

package org.acme.chat;

import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.inject.Inject;

@WebSocket(path = "/echo")
public class EchoSocket {

    @Inject
    WebSocketConnection connection;

    @OnTextMessage
    public void onTextMessage(String message) {
        // send a message back to this same connection
        connection.sendText(message).subscribe().with(v -> {}, Throwable::printStackTrace);
    }
}

sendText/sendBinary return a Uni<Void> that completes once the frame has actually been written, which is also how outgoing back-pressure is surfaced — see below. connection.close() closes just this connection; connection.id() returns a unique identifier for it; connection.pathParam("room") reads a path parameter (see below) for use outside the callback’s own parameter list.

Broadcasting to all connections, or a subset

Every open connection to a given @WebSocket endpoint is tracked automatically, and WebSocketConnection exposes a broadcast()/broadcast().sendText(…​) API that fans a message out to some or all of them without the endpoint needing to keep its own registry:

@OnTextMessage
public void onTextMessage(String message) {
    // every connection currently open to this endpoint path, this one included
    connection.broadcast().sendText(message)
            .subscribe().with(v -> {}, Throwable::printStackTrace);
}

To exclude the sender and target only the other connections in the same room — the shape of a typical chat broadcast — filter the broadcast by path parameter or by any other predicate over WebSocketConnection:

package org.acme.chat;

import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.inject.Inject;

@WebSocket(path = "/chat/{room}")
public class ChatSocket {

    @Inject
    WebSocketConnection connection;

    @OnTextMessage
    public void onTextMessage(String message) {
        String room = connection.pathParam("room");
        connection.broadcast()
                .filter(c -> room.equals(c.pathParam("room")) && !c.id().equals(connection.id()))
                .sendText(message)
                .subscribe().with(v -> {}, Throwable::printStackTrace);
    }
}

Path parameters

A {placeholder} segment in the endpoint’s path — /chat/{room} above — binds by name either to a method parameter of a lifecycle callback or, as shown above, via connection.pathParam(…​):

@OnOpen
public void onOpen(@io.quarkus.websockets.next.PathParam("room") String room) {
    // "room" is the {room} segment of the connection's path
}

Sub-protocol negotiation

An endpoint declares the WebSocket sub-protocols it supports, and Quarkus negotiates Sec-WebSocket-Protocol against whatever the client offered during the handshake:

@WebSocket(path = "/chat/{room}", subprotocols = { "chat.v2", "chat.v1" })
public class ChatSocket {
    // ...
}

The first entry the client also offers wins; the negotiated value is available from the connection’s handshake request if the endpoint needs to branch behavior on it.

Security

WebSockets Next reuses the same HTTP security layer as the rest of Quarkus: authentication happens during the HTTP upgrade request — the plain HTTP request a WebSocket connection starts life as, before it is upgraded to the WebSocket protocol — so any mechanism already configured for regular endpoints (HTTP Basic, form-based, OIDC bearer tokens, mTLS) applies unchanged; see Security Overview and Architecture and Security: Authentication Mechanisms for how each of those mechanisms actually extracts and validates credentials.

Once the upgrade succeeds, the resulting SecurityIdentity is associated with the connection for its entire lifetime — not just the initial request — and can be injected or checked from any lifecycle callback:

package org.acme.chat;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.WebSocket;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;

@WebSocket(path = "/chat/{room}")
@RolesAllowed("chat-user")
public class ChatSocket {

    @Inject
    SecurityIdentity identity;

    @OnOpen
    public void onOpen() {
        String user = identity.getPrincipal().getName();
        // identity stays the same for every callback on this connection, for its whole lifetime
    }
}

@RolesAllowed, @Authenticated, and the other standard annotations work directly on the @WebSocket-annotated class (denying the upgrade itself if the check fails) or on individual callback methods.

Per-connection state

Because Quarkus instantiates one bean per connection (as noted above), instance fields on a @WebSocket bean are effectively connection-scoped — there is exactly one ChatSocket instance handling exactly one connection’s callbacks, so a field like private int messageCount; accumulates per-connection state safely with no extra scope annotation needed:

@WebSocket(path = "/chat/{room}")
public class ChatSocket {

    private int messageCount;

    @OnTextMessage
    public void onTextMessage(String message) {
        messageCount++;
        // safe: this field belongs to this connection's own bean instance only
    }
}

This is the opposite of the default CDI scope most Quarkus code is written against. A typical @ApplicationScoped REST resource has exactly one instance shared across every request and every client, so an instance field there is shared, mutable state that needs explicit synchronization or an external store. A @WebSocket endpoint bean, by contrast, defaults to one instance per connection — so the same pattern that would be a concurrency bug on a REST resource is the normal, intended way to keep per-client state here. Do not add @ApplicationScoped to a @WebSocket bean expecting REST-resource semantics; it changes this guarantee.

Back-pressure on outgoing messages

WebSocketConnection.sendText/sendBinary return Uni<Void> (a single result), and the connection also exposes a Multi-based sending API for streaming a sequence of frames while respecting the client’s ability to keep up:

package org.acme.chat;

import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Multi;
import jakarta.inject.Inject;

@WebSocket(path = "/ticks")
public class TickSocket {

    @Inject
    WebSocketConnection connection;

    @OnOpen
    public Multi<String> streamTicks() {
        // returning a Multi from @OnOpen streams it out; Quarkus only requests the next
        // item once the previous frame has actually been written to the socket
        return Multi.createFrom().ticks().every(java.time.Duration.ofSeconds(1))
                .map(tick -> "tick " + tick);
    }
}

Returning a Multi from a lifecycle callback is the recommended way to stream a sequence of outgoing messages, because the underlying Vert.x WebSocket write is only resumed once the previous frame’s write completes — which is exactly the back-pressure signal a slow or congested client needs to avoid the server buffering unbounded outgoing data in memory. Calling sendText/sendBinary in a tight loop without awaiting the returned Uni bypasses that signal and should be avoided for any high-volume sender.

The client-side API

WebSockets Next is not only a server-side model — the same extension lets a Quarkus application connect out to another WebSocket endpoint, using client-side annotations that mirror the server ones:

package org.acme.chat;

import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocketClient;
import io.quarkus.websockets.next.WebSocketClientConnection;
import jakarta.inject.Inject;

@WebSocketClient(path = "/chat/{room}")
public class ChatClient {

    @Inject
    WebSocketClientConnection connection;

    @OnOpen
    public void onOpen() {
        // connected to the remote endpoint
    }

    @OnTextMessage
    public void onTextMessage(String message) {
        // a message arrived from the remote endpoint
    }

    @OnClose
    public void onClose() {
        // remote endpoint closed the connection, or it was closed locally
    }
}

A connection is opened explicitly, supplying the base URI and any path parameters, through the injectable WebSocketConnector:

package org.acme.chat;

import io.quarkus.websockets.next.WebSocketConnector;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.net.URI;

@ApplicationScoped
public class ChatClientStarter {

    @Inject
    WebSocketConnector<ChatClient> connector;

    public void connect() {
        connector.baseUri(URI.create("wss://remote-host/chat"))
                .pathParam("room", "lobby")
                .connectAndAwait();
    }
}

The client connection object (WebSocketClientConnection, injectable the same way as WebSocketConnection on the server side) exposes the same sendText/sendBinary/close API for talking back to the remote endpoint.

Testing

@QuarkusTest starts the same HTTP endpoint a packaged application would serve, so a WebSocket endpoint is exercised with a real client connecting to it over ws://localhost:<test-port> — either the WebSocketClient annotated bean shown above pointed at the test server’s URI, or the lower-level Vert.x WebSocketClient API for finer control over frames and timing. See Testing Quarkus Applications for how @QuarkusTest, injected test resources, and @TestHTTPResource-style port/URI injection are wired together in general; the same conventions apply unchanged to a test that opens a WebSocket connection instead of an HTTP client call.

The legacy quarkus-websockets extension. Before WebSockets Next, Quarkus shipped WebSocket support through the quarkus-websockets extension, built directly on the Jakarta/Undertow jakarta.websocket.* API (@ServerEndpoint, Session, @OnMessage) rather than a CDI bean-per-connection model. It still appears in codebases predating the rewrite, and in a handful of extensions or integration patterns that have not yet been migrated to the new model. For any new endpoint, quarkus-websockets-next — the @WebSocket model described on this page — is the current, recommended choice, and the two extensions are not meant to be mixed on the same endpoint path.

How a broadcast reaches every client in a room

The sequence below traces the scenario used throughout this page: two clients open connections to the same /chat/{room} endpoint, one sends a text message, and the server broadcasts it to the other connections sharing that room.

sequenceDiagram participant ClientA as Client A participant ClientB as Client B participant Endpoint as ChatSocket (bean per connection) ClientA->>Endpoint: HTTP upgrade to /chat/lobby activate Endpoint Endpoint-->>Endpoint: @OnOpen (Client A's instance) deactivate Endpoint ClientB->>Endpoint: HTTP upgrade to /chat/lobby activate Endpoint Endpoint-->>Endpoint: @OnOpen (Client B's instance) deactivate Endpoint ClientA->>Endpoint: text frame "hello room" activate Endpoint Endpoint-->>Endpoint: @OnTextMessage (Client A's instance) Endpoint-->>Endpoint: connection.broadcast().filter(room, not self).sendText(...) Endpoint->>ClientB: text frame "hello room" deactivate Endpoint ClientB-->>ClientB: @OnTextMessage (Client B's instance) receives it

For using WebSockets or SSE to stream chat responses to a front end, see Streaming Chat APIs.