Real-Time Systems is one of the most misunderstood terms in software. It is not about speed; it is about correctness under a deadline. In a true real-time system, a late answer is a wrong answer. But most developers encounter the phrase in the context of web applications — chat, collaborative editing, live dashboards — where “real-time” means the UI updates without a manual refresh. That gap between the textbook definition and everyday practice is where projects sink or swim.
This article is about the latter: building real-time applications with WebSockets, Server-Sent Events (SSE), WebRTC, and voice assistants. We will look at the mechanisms, walk through a concrete task, expose where the scheme breaks, and compare learning paths. No magic, no invented statistics — just the engineering that separates a demo from a production system.
What “real-time” actually means in web applications
In web development, real-time is a communication pattern, not a performance guarantee. The server pushes data to the client as events happen, instead of the client polling on a timer. The mechanism matters because each transport has different costs and failure modes.
-
WebSockets (RFC 6455) give you a full-duplex TCP connection. The client and server can send messages at any time. This is the default choice for chat, multiplayer games, and collaborative editing.
-
Server-Sent Events (SSE, part of the HTML Living Standard) give you a one-way stream from server to client over plain HTTP. It reconnects automatically and works through most proxies. Perfect for notifications, stock tickers, or progress bars — anything where the client does not need to push.
-
WebRTC (series of IETF RFCs, e.g., RFC 8825 for overview) is for peer-to-peer audio, video, and data channels. It handles NAT traversal with ICE, STUN, and TURN. This is the only way to do low-latency voice/video without routing every packet through your server.
-
Voice assistants built on these primitives: you stream audio chunks over WebSocket or WebRTC, run speech-to-text, process the intent, and stream the response back. The hard part is not the AI; it is the latency budget and the handling of partial results.
A common myth: “WebSockets are always faster.” In reality, SSE often wins for one-way updates because it runs over HTTP/2 and avoids the upgrade handshake. Another myth: “real-time means no polling.” Long polling is still a valid fallback when corporate proxies block WebSocket upgrades — and you will meet those proxies.
A concrete walkthrough: building a collaborative document editor
Let us take a realistic task: multiple users edit the same text document simultaneously, and changes appear within a second. This is where real-time systems get interesting because you must solve conflict resolution and state synchronization.
Step 1: choose the transport
For a web editor, WebSocket is the natural fit: bidirectional, low overhead per message. You could use SSE for server-to-client updates and a separate HTTP POST for client-to-server, but that doubles the connections and complicates ordering.
So: one WebSocket per active document. The server maintains a room per document ID. Clients send operations (insert, delete, format) as JSON messages.
Step 2: pick a consistency model
You cannot just broadcast operations and apply them in arrival order — two users might insert at the same position, and the result depends on network timing. You need a conflict-free replicated data type (CRDT) or operational transformation (OT).
-
OT transforms operations against concurrent ones. It requires a central server to order operations. Google Docs historically used OT. The trade-off: simpler client logic, but the server becomes a bottleneck and a single point of failure.
-
CRDT makes operations commutative and idempotent by design. No central ordering needed. The trade-off: metadata overhead grows with document size, and some CRDTs (like certain tree-based ones) have complex garbage collection.
For a text editor, a sequence CRDT (e.g., Yjs or Automerge) is often the pragmatic choice. You pay in memory and message size; you gain offline support and peer-to-peer potential.
Step 3: design the message flow
Each client sends an operation with a unique ID and a logical timestamp (Lamport clock or vector clock). The server appends it to the document log and broadcasts to all other clients in the room. Clients apply operations in causal order.
A concrete example: user A inserts “hello” at position 0. User B, concurrently, inserts “world” at position 0. With a naive last-write-wins, one insertion is lost. With a CRDT, both survive, and the final string is deterministic (e.g., “helloworld” or “worldhello” depending on tie-breaking).
Step 4: handle presence and awareness
Users need to see cursors and selections of others. This is ephemeral state — it does not belong in the document log. Use a separate lightweight channel over the same WebSocket: send cursor updates at most every 100 ms, and let the server broadcast them without persistence. If a client disconnects, its presence disappears after a timeout.
Step 5: scale the server
A single Node.js process can handle a few thousand WebSocket connections, but you need horizontal scaling. Two common patterns:
-
Pub/Sub with Redis for small to medium scale. Each server instance subscribes to a channel per document. When an operation arrives, the instance publishes to Redis; all instances (including the sender) receive and broadcast to their local clients. Redis Pub/Sub is fire-and-forget: if a subscriber is down, it misses messages. For critical operations, use Redis Streams or a message queue with persistence.
-
NATS or Kafka for higher throughput and durability. Kafka gives you a replayable log, which is useful for auditing and for recovering a crashed server. The cost: operational complexity and higher latency (batching).
Step 6: secure the connection
Always use WSS (WebSocket over TLS). Authenticate during the HTTP upgrade handshake — pass a short-lived token in a query parameter or a cookie. Do not rely on the first message for auth; by then the connection is already established. Validate every operation on the server: a malicious client could send a malformed CRDT update that corrupts the document. Rate-limit messages per connection.
If you are storing documents in a BaaS like Firestore or Supabase, remember that security rules must also cover real-time listeners. A misconfigured rule can leak data through a subscription just as easily as through a REST call. Our article on Firestore Security Rules and Supabase RLS digs into that.
Where the real-time scheme breaks
Even a well-designed system has failure modes. Here are four scenarios where the mechanism itself fails, not just the implementation.
1. The proxy that eats WebSocket upgrades
What goes wrong: Corporate firewalls and some CDNs terminate or block the HTTP Upgrade handshake. The client falls back to long polling or fails silently. Users see stale data.
How to spot it early: Instrument connection success rate by user agent and network. A sudden drop in WebSocket connections from a specific region or ASN is a red flag. Also, test behind a proxy that strips the Upgrade header.
What to do if it is too late: Implement a fallback transport (SSE or long polling) from day one. Libraries like Socket.IO do this automatically, but they add their own protocol overhead. If you built raw WebSocket, you need a feature flag to switch transports.
2. The CRDT metadata balloon
What goes wrong: A document edited for months accumulates tombstones and operation metadata. Memory usage grows linearly with edit history. Eventually, loading the document takes seconds and the client crashes on low-end devices.
How to spot it early: Monitor document size and load time. Set a threshold: if a document exceeds, say, 5 MB of CRDT state, trigger compaction.
What to do if it is too late: Implement snapshotting: serialize the current state, discard the operation log, and start a new epoch. Clients that were offline during the snapshot must resync from scratch. This is a complex migration — better to design for it upfront.
3. The voice assistant latency cliff
What goes wrong: You stream audio to a speech-to-text service, get a transcript, call an LLM, then synthesize speech. Each step adds latency. If total round-trip exceeds roughly 300–500 ms, conversation feels unnatural. Users start talking over the assistant.
How to spot it early: Measure end-to-end latency per stage. If STT alone takes 200 ms, you have little budget left for the rest.
What to do if it is too late: Use streaming STT and streaming TTS. Start synthesizing the response before the full text is ready. Implement barge-in: if the user speaks while the assistant is talking, stop playback immediately. This requires echo cancellation on the client and a separate audio channel for control messages.
4. The Pub/Sub message loss
What goes wrong: You use Redis Pub/Sub for broadcasting operations. A server instance crashes or loses connection briefly. Messages published during that window are lost. Clients on that instance miss updates and diverge.
How to spot it early: Compare document state hashes between clients periodically. A mismatch indicates lost messages. Also, monitor Redis subscriber count and connection stability.
What to do if it is too late: Switch to a durable log (Redis Streams, NATS JetStream, Kafka). Clients must be able to request missing operations by sequence number. This is essentially implementing a replication protocol — significant work.
Learning paths: self-study, courses, and practice
How do people actually learn to build real-time systems? There are three broad paths, each with trade-offs.
Self-study — reading RFCs, blog posts, and open-source code. Cost: near zero in money, high in time. You can learn the basics of WebSockets in a weekend. But CRDTs, OT, and distributed messaging are deep waters. Without feedback, you will not know if your design is correct until it fails in production. You also miss the tacit knowledge: why certain defaults exist, what breaks at scale, how to debug a divergence.
Structured courses — guided paths with explanations and exercises. Cost: moderate money, lower time. A good course compresses the learning curve by showing you the pitfalls upfront. The risk: courses that stay at the “hello world” level and never touch production concerns. Look for ones that cover failure modes, scaling, and security.
Practice on real projects — building a chat or collaborative tool, even for yourself. Cost: time, plus the cost of mistakes. This is where the knowledge sticks. But without mentorship, you may reinforce bad patterns. Pair it with code reviews or open-source contributions.
A realistic combination: start with a structured course to get the map, then build a small project, then read the specs and source code of libraries like Yjs or Socket.IO to see how the pros do it.
If you are also considering backend performance, our breakdown of Go for Backend (gRPC) covers similar trade-offs in a different context.
Who this path suits — and who it does not
Real-time systems are not for everyone. This path suits developers who:
- Enjoy reasoning about concurrency, ordering, and state.
- Are comfortable with networking fundamentals (TCP, TLS, HTTP).
- Want to work on products where live interaction is core: collaboration tools, chat, gaming, voice interfaces.
It does not suit those who:
- Prefer purely visual or front-end-only work with no backend logic.
- Want a quick win without dealing with distributed systems concepts.
- Are uncomfortable with debugging intermittent failures — real-time bugs are often timing-dependent and hard to reproduce.
If you are in the first group, the field is rewarding: real-time features are increasingly expected, and the engineering challenges are genuine. If you are in the second, you can still use real-time APIs, but you may not need to build the underlying machinery.
Conclusion
Real-time systems are about correctness under constraints — not just speed. The myths (“WebSockets are always better”, “real-time means no polling”) lead to fragile designs. The reality is a set of trade-offs: transport, consistency model, scaling pattern, and failure handling. Mastering them takes guided practice and honest feedback on your decisions.
If you want a structured path that walks through these trade-offs with examples and gives you feedback on your designs, the course Real-Time Systems covers WebSockets, SSE, WebRTC, CRDT/OT, Pub/Sub scaling, and security — from first principles to production patterns. Start there, then build something small and break it. That is how the knowledge sticks.