Skip to main content

Command Palette

Search for a command to run...

Real-Time Communication in Quarkus: SSE or WebSocket?

Updated
8 min read
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 / AspectServer-Sent Events (SSE)WebSockets
Communication DirectionOne-way (Server → Client only)Two-way (Client ↔ Server)
Protocol BaseStandard HTTP/HTTPS (works with HTTP/1.1 & HTTP/2)Custom WebSocket protocol (after HTTP upgrade handshake)
Connection EstablishmentSimple HTTP GET requestRequires handshake to upgrade from HTTP to WebSocket
Data FormatText only (UTF-8, event/data fields)Text or binary (flexible framing)
Automatic ReconnectionBuilt-in (EventSource retries automatically)Must be implemented manually (heartbeat/reconnect logic)
Browser SupportWidely supported (except legacy IE/Edge)Widely supported in modern browsers
Firewall/Proxy FriendlinessVery high (uses standard ports 80/443, HTTP semantics)Can be blocked by strict firewalls/proxies (non-HTTP protocol)
Client → Server MessagingNot supported (needs separate HTTP calls like fetch/Ajax)Natively supported (send() method)
ComplexityLow (simple API, browser handles reconnection)Higher (manage state, heartbeats, message framing)
PerformanceEfficient for server push, but limited to textVery efficient, supports high-frequency, low-latency data exchange
Typical Use CasesNotifications, stock tickers, news feeds, dashboardsChat 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

Real-Time Web Communication with SSE and WebSocket (Demo & Sample)