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
|
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 |
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.
For using WebSockets or SSE to stream chat responses to a front end, see Streaming Chat APIs.
References
-
Quarkus Guides — WebSockets (legacy Undertow-based extension, for the migration note)