"Real-time" covers a lot of ground — a chat app, a live dashboard, presence indicators, and collaborative cursors have very different needs. Picking the wrong transport or architecture leads to features that work in a demo and fall apart under real load. The good news: most real-time needs map cleanly to a few well-understood patterns. Here's how to choose.
Match the transport to the need
Start by asking: does data flow one way (server to client) or both ways?
- Server-Sent Events (SSE): a simple, one-way stream from server to client over plain HTTP. Perfect for notifications, live feeds, activity streams, and dashboards. It auto-reconnects, works through most proxies, and is far simpler than WebSockets. Underused and often the right answer.
- WebSockets: a persistent, full-duplex connection for true bidirectional, low-latency communication. The right choice for chat, multiplayer, collaborative editing, and live cursors — anything where the client sends frequently too.
- Polling / long-polling: ask the server repeatedly. Crude, but occasionally the pragmatic choice for infrequent updates or constrained environments. Don't reach for it by default.
The rule of thumb: if clients only receive, use SSE; if clients also send constantly, use WebSockets.
Don't build it from scratch if you don't have to
Persistent connections are easy to start and hard to operate. Before writing your own WebSocket server, consider managed layers that solve the hard parts (scaling, presence, fan-out, reconnection):
- Supabase Realtime — if you're already on Postgres, it broadcasts database changes and presence over WebSockets with almost no server code
- Managed pub/sub services (Pusher, Ably, and similar) handle connection scaling and delivery guarantees for you
- Your framework may have a batteries-included option worth using
Building your own is justified when you have very specific requirements or scale economics — not by default.
The architecture that scales
The hard part of real-time isn't one connection — it's ten thousand. A single server holds a limited number of connections, and once you run multiple servers, a client connected to server A must receive messages published on server B.
The standard solution is a pub/sub backbone (often Redis) sitting behind your connection servers:
- Connection servers hold the client WebSocket/SSE connections
- When something happens, it's published to a channel on the pub/sub layer
- Every connection server subscribes and pushes to its relevant clients
This decouples "who's connected where" from "what needs broadcasting," and lets you scale connection servers horizontally. Managed real-time services implement this pattern so you don't have to.
Handle the messy realities
Real-time features live or die on their handling of failure:
- Reconnection. Connections drop constantly (mobile networks, sleep, proxies). Clients must reconnect with backoff automatically.
- Missed messages. After a reconnect, the client may have missed updates. Support catch-up — resend since a cursor/timestamp, or refetch state on reconnect.
- Presence. "Who's online" needs heartbeats and timeouts to detect silent disconnects.
- Ordering and conflicts. For collaboration, decide how to resolve simultaneous edits (last-write-wins, or CRDTs/OT for real collaborative text).
- Authentication. Authenticate the connection at handshake and authorize each subscription — don't trust the client's channel claims.
The pragmatic path
Use the simplest transport that meets the need (SSE far more often than teams assume), lean on a managed real-time layer unless you have a strong reason not to, and design for reconnection and missed messages from day one. Most "real-time is hard" horror stories come from skipping that last part.
If you're adding real-time features and want an architecture that stays reliable past the demo, let's talk.
Originally published on the Doktouri Agency blog. We build web, mobile, SaaS, and AI products — let's talk.
Top comments (0)