Real-Time Systems: Myths, Realities, and the Engineering Behind Live Apps

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.

Key distinction Real-time in web apps is about push vs. pull. Choose the transport based on direction, latency tolerance, and infrastructure constraints — not on hype.

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.

What to check first Before writing any sync logic, decide on the consistency model. Retrofitting CRDT into an OT-based system (or vice versa) is a rewrite, not a refactor.

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.

The common thread Real-time failures are rarely about raw speed. They are about state divergence, transport fallbacks, and resource growth over time. Design for these from the start.

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.

Where self-study stalls The hardest part is not writing a WebSocket server. It is knowing whether your CRDT merge is correct, your fallback is seamless, and your scaling plan will hold. That requires feedback you cannot easily give yourself.

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.

← All posts

Ask AI about this article

Ask a question about this article — ASI Biont will answer based on its text.