Skip to content

Patterns & Alternatives

By now you can open a socket, design messages, keep a connection alive, and broadcast from a server. This final module zooms out from mechanics to shapes. Once you have watched a few real-time systems get built, you notice the same handful of patterns appearing again and again — and you notice that a WebSocket is sometimes not even the right tool. This lesson maps that territory before the rest of the module fills it in.

A persistent, two-way connection is a blank canvas. That freedom is exactly why teams accidentally reinvent the same structures. Three of them cover the overwhelming majority of real-time features:

  • Publish / subscribe. A client declares interest in some topics — a chat room, a stock symbol, a document id — and the server delivers only the events that match. Nobody receives a firehose of everything; each client gets a tailored slice. This is the backbone of almost every multi-user app.
  • Request / response (RPC). Sometimes the client genuinely wants an answer to this specific question. Over a one-way message pipe there is no built-in “reply to this”, so you build one: tag the request with an id and resolve a promise when the reply with the same id arrives.
  • Fire-and-forget events. A client emits something — “cursor moved to (x, y)”, “user is typing” — and expects no reply at all. The cheapest pattern, and the one WebSockets were practically designed for.

Most production protocols are just these three composed together over one connection: subscribe to a room, RPC to fetch its history, then fire-and-forget every keystroke.

Before reaching for a WebSocket reflexively, it is worth asking what the data actually needs. The honest answer is often “less than a full duplex socket”. A quick way to decide:

flowchart TD
  start["Does the client need to send<br/>messages mid-stream too?"]
  start -- "No, only receive" --> sse["Server-Sent Events<br/>(simple, auto-reconnect, HTTP)"]
  start -- "Yes, both directions" --> freq["High-frequency or<br/>unordered media/datagrams?"]
  freq -- "No, ordinary messages" --> ws["WebSocket<br/>(bidirectional, ordered)"]
  freq -- "Yes" --> wt["WebTransport<br/>(HTTP/3, streams + datagrams)"]
  sse -. "no SSE / proxy strips it" .-> poll["Long-polling fallback"]
  ws -. "blocked by proxy" .-> poll
Choosing a real-time transport by what the data needs

The point is not that WebSockets are bad — they are the default for genuinely bidirectional traffic. The point is that “real-time” is not one requirement. A live price ticker only flows server-to-client, so Server-Sent Events are simpler and reconnect themselves. A multiplayer game tolerates dropped, out-of-order packets, so WebTransport datagrams fit better. Match the transport to the traffic and you carry less complexity.

This module — Patterns & Alternatives — turns those shapes into things you can build and choose between:

  1. Patterns & Alternatives (you are here) — the common patterns and how to pick a transport.
  2. Pub/sub and rooms — subscribe to topics, receive only what you asked for, with a runnable in-page demo.
  3. RPC over WebSocket — correlation ids and promises that resolve on the matching reply, with timeouts.
  4. WebSocket vs SSE vs WebTransport — choosing between the bidirectional socket, the one-way stream, the fallback, and the new HTTP/3 transport.
  5. Building a real-time app — tie rooms, heartbeats, auth, and reconnect together into one small service on a real ws server.

Everything earlier was a building block; this module is the assembly instructions. If a concept here feels thin, the foundations are one click away: the client API, message design, the connection lifecycle, server design, and scaling with pub/sub all feed directly into what we build next.

Which three patterns cover most real-time features built over one connection?
When the client only needs to receive a one-way stream and never send mid-connection, which transport is the simpler fit?
What is the core idea behind the publish/subscribe pattern?