Real-Time Communication in Quarkus: SSE or WebSocket?

Preface
There is a moment every backend developer encounters.
You refresh the page. Nothing changes. You refresh again. Still nothing.
And then the question appears:
“Why does my app only speak when I ask?”
Modern users expect more. Dashboards update by themselves. Notifications arrive without clicking refresh. Systems feel alive.
That moment is usually when developers discover Server-Sent Events (SSE) and WebSocket.
This article is a beginner-friendly journey through both technologies, grounded in a Quarkus-based project, and focused on why they exist—not just how to use them.
1. Before Real-Time: The Problem We All Had
The web was built on a simple promise:
Client → Request → Server
Client ← Response ← Server
It worked beautifully—for documents.
But problems emerged when applications needed to:
show live metrics
display notifications
update dashboards continuously
support interactive collaboration
The workaround was ugly:
refresh loops
aggressive polling
long-polling hacks
Bandwidth wasted. Servers overloaded. UX suffered. Real-time communication wasn’t a luxury anymore—it was survival.
2. Enter SSE and WebSocket: The Heroes We Needed
To solve these problems, two technologies emerged: Server-Sent Events (SSE) and WebSocket.
SSE became part of HTML5 around 2011, designed for:
live feeds
notifications
monitoring dashboards
It intentionally avoided complexity.
WebSocket, standardized in 2011 as well, offered a full-duplex communication channel, ideal for:
chat applications
multiplayer games
collaborative tools
It allowed both client and server to send messages independently.
3. Understanding Server-Sent Events (SSE)
SSE is a unidirectional protocol where the server pushes updates to the client over a single HTTP connection, and built on top of standard HTTP, making it easy to implement and compatible with existing infrastructure.
The flow looks like this:
Browser
|
| (HTTP request for SSE)
v
Server
|=====> (streaming events) =====>
|
Browser
Key ideas:
One-way communication (server → client)
Built on plain HTTP
Automatic reconnection
Very little client-side code
4. Understanding WebSocket
WebSocket is a full-duplex protocol that allows both the client and server to send messages independently over a single, long-lived connection.
The flow looks like this:
Browser
|\
| \ (WebSocket handshake)
v \
Server
|<==== bidirectional messages ====>
|
Browser
Key ideas:
Two-way communication (client ↔ server)
Requires a handshake to upgrade from HTTP
Low latency, real-time interaction
More complex client and server implementations
5. When to Use SSE vs. WebSocket
Choosing between SSE and WebSocket depends on your application's needs:
Use SSE when:
You need simple, one-way updates from server to client.
Your application is read-heavy (e.g., live news feeds, stock tickers).
You want to leverage existing HTTP infrastructure.
Use WebSocket when:
You need two-way communication.
Your application is interactive (e.g., chat apps, multiplayer games).
Low latency is critical.
6. Comparison Table
| Feature / Aspect | Server-Sent Events (SSE) | WebSockets |
| Communication Direction | One-way (Server → Client only) | Two-way (Client ↔ Server) |
| Protocol Base | Standard HTTP/HTTPS (works with HTTP/1.1 & HTTP/2) | Custom WebSocket protocol (after HTTP upgrade handshake) |
| Connection Establishment | Simple HTTP GET request | Requires handshake to upgrade from HTTP to WebSocket |
| Data Format | Text only (UTF-8, event/data fields) | Text or binary (flexible framing) |
| Automatic Reconnection | Built-in (EventSource retries automatically) | Must be implemented manually (heartbeat/reconnect logic) |
| Browser Support | Widely supported (except legacy IE/Edge) | Widely supported in modern browsers |
| Firewall/Proxy Friendliness | Very high (uses standard ports 80/443, HTTP semantics) | Can be blocked by strict firewalls/proxies (non-HTTP protocol) |
| Client → Server Messaging | Not supported (needs separate HTTP calls like fetch/Ajax) | Natively supported (send() method) |
| Complexity | Low (simple API, browser handles reconnection) | Higher (manage state, heartbeats, message framing) |
| Performance | Efficient for server push, but limited to text | Very efficient, supports high-frequency, low-latency data exchange |
| Typical Use Cases | Notifications, stock tickers, news feeds, dashboards | Chat apps, online games, collaborative editing, IoT, real-time trading |
7. Implementing SSE and WebSocket in Quarkus
SSE Example in Quarkus
This design is a Quarkus SSE (Server‑Sent Events) resource that continuously streams JSON events to connected clients
The architecture looks like this:
+--------------------+ HTTP (text/event-stream) +-----------------------+
| Browser | -------------------------------> | Quarkus SSE Resource |
| Web Component | GET /sse/stream | streams events |
| EventSource() | <------------------------------- | every 1s |
+--------------------+ continuous stream +-----------------------+
The server implementation:
This Quarkus resource exposes an SSE endpoint at /sse/stream. It uses a reactive Multi to emit a tick every second, mapping each tick into a JSON object with an event name and timestamp. Because the method produces text/event-stream and specifies application/json, clients receive a continuous stream of JSON messages over a single HTTP connection
@Path("/sse")
public class SseResource {
@GET
@Path("/stream")
@Produces("text/event-stream")
@RestStreamElementType("application/json")
public Multi<Map<String, String>> streamEvents() {
return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
.map(tick -> Map.of(
"event", "tick",
"time", DateTimeFormatter.ISO_INSTANT.format(Instant.now())
));
}
}
The client implementation:
The following HTML snippet defines a custom web component that connects to the SSE endpoint and updates its content with the received time every second.
<script>
class TimeStream extends HTMLElement {
connectedCallback() {
this.innerHTML = `<p>Waiting for time...</p>`;
const es = new EventSource("http://localhost:8080/sse/stream");
es.onmessage = (event) => {
const data = JSON.parse(event.data);
this.querySelector("p").textContent = data.time;
};
}
}
customElements.define("time-stream", TimeStream);
</script>
<body>
<h1>Hello {name}!</h1>
<time-stream></time-stream>
</body>
Demo page:

WebSocket Example in Quarkus
This design is a Quarkus WebSocket resource that enables bidirectional communication between clients and the server.
The architecture looks like this:
+--------------------+ WebSocket (Client ↔ Server) +-----------------------+
| Browser | -------------------------------> | Quarkus WebSocket |
| Web Component | GET /websocket | endpoint |
| WebSocket() | <------------------------------- | |
+--------------------+ bidirectional stream +-----------------------+
The server implementation:
The following Java class defines a WebSocket endpoint at /chatEndPoint. It handles connection events, incoming messages, and errors, logging relevant information for each event.
@Slf4j
@ServerEndpoint("/chatEndPoint")
@ApplicationScoped
public class ChatEndPoint {
@OnOpen
public void onOpen(Session session) {
log.info("WebSocket opened: {}", session.getId());
}
@OnMessage
public String onMessage(String message, Session session) {
log.info("Received from {}: {}", session.getId(), message);
return "Echo: " + message;
}
@OnClose
public void onClose(Session session) {
log.info("WebSocket closed: {}", session.getId());
}
@OnError
public void onError(Session session, Throwable throwable) {
log.error("Error in session {}: {}", session.getId(), throwable.getMessage(), throwable);
}
}
The client implementation:
The following HTML snippet defines a custom web component that connects to the WebSocket endpoint, allowing users to send messages and receive echoed responses from the server.
<html>
<script type="module">
import "/ws-chat/ws-chat.js";
</script>
<body>
<ws-chat url="ws://localhost:8080/chatEndPoint"></ws-chat>
</body>
</html>
The custon web component implementation(by Lit Framework):
import {
LitElement,
html,
css,
} from "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js";
export class WsChat extends LitElement {
static properties = {
url: { type: String }, // <ws-chat url="ws://...">
connected: { type: Boolean, state: true },
messages: { type: Array, state: true },
};
constructor() {
super();
this.url = ""; // default computed in connect()
this.connected = false;
this.messages = [];
this._ws = null;
}
connectedCallback() {
super.connectedCallback();
this._connect();
}
disconnectedCallback() {
super.disconnectedCallback();
this._close();
}
updated(changed) {
// If url attribute changes at runtime, reconnect
if (changed.has("url")) {
this._connect(true);
}
}
render() {
return html`
<div class="chat">
<div class="output">${this.messages.map((m) => html`<p>${m}</p>`)}</div>
<div class="input-row">
<input @keydown=${this._onKeyDown} placeholder="Type a message..." />
<button ?disabled=${!this.connected} @click=${this.sendMessage}>
Send
</button>
</div>
</div>
`;
}
_onKeyDown = (e) => {
if (e.key === "Enter") this.sendMessage();
};
_add(msg) {
this.messages = [...this.messages, msg];
// auto-scroll after render
this.updateComplete.then(() => {
const out = this.renderRoot.querySelector(".output");
out.scrollTop = out.scrollHeight;
});
}
_defaultUrl() {
const proto = location.protocol === "https:" ? "wss" : "ws";
return `${proto}://${location.host}/ws`;
}
_connect(force = false) {
const target = this.url?.trim() || this._defaultUrl();
if (!force && this._ws && this._ws.readyState === WebSocket.OPEN) return;
this._close();
this.connected = false;
this._ws = new WebSocket(target);
this._ws.onopen = () => {
this.connected = true;
this._add("Connected to server.");
};
this._ws.onclose = () => {
this.connected = false;
this._add("Disconnected from server.");
};
this._ws.onmessage = (e) => this._add("Server: " + e.data);
}
_close() {
if (!this._ws) return;
try {
this._ws.close(1000);
} catch {}
this._ws = null;
}
sendMessage = () => {
const input = this.renderRoot.querySelector("input");
const msg = input.value.trim();
if (!msg || !this._ws || this._ws.readyState !== WebSocket.OPEN) return;
this._ws.send(msg);
this._add("You: " + msg);
input.value = "";
input.focus();
};
}
customElements.define("ws-chat", WsChat);
Demo page:

8. key Takeaways
SSE and WebSocket are powerful tools for real-time web applications, each with its strengths and ideal use cases.
SSE is perfect for simple, one-way server-to-client updates, while WebSocket excels in interactive, two-way communication.
Quarkus makes it easy to implement both SSE and WebSocket, allowing developers to build modern, responsive applications that meet user expectations for real-time interactivity.
9. Conclusion
Real‑time communication is no longer a luxury — it has become an expectation. The challenge is not to reach for the most powerful tool by default, but to select the one that best fits the need.
When your system primarily delivers information to users, Server‑Sent Events (SSE) offer an elegant and efficient solution. When interaction flows both ways and users need to respond in real time, WebSockets provide the right abstraction.
With Quarkus, both approaches are accessible, performant, and ready for production, giving you the flexibility to choose wisely without compromise.
10. Resources
you can find the full code examples on my GitHub repository: sse-and-websocket-demo



