Building Real-Time Applications with WebSockets and Event-Driven Architecture
Live dashboards, chat, and notifications all need the server to push updates instead of waiting to be asked. Here's how WebSockets and event-driven design make that reliable.
Sarah Chen
· 3 min read
Most web traffic is still request-response: the client asks, the server
answers. That model breaks down for anything that needs to feel “live” —
a chat window, a dashboard that updates as data changes, a notification
that appears the moment something happens elsewhere in the system.
Real-time features need the server to push, not just respond.
Why polling isn’t the answer
The naive fix — have the client ask “anything new?” every few seconds —
works for a demo and falls apart under real load: most requests return
“no,” you’re paying the cost of constant round-trips, and updates are
only ever as fresh as your polling interval. It also doesn’t scale
gracefully; more active users means proportionally more wasted requests.
What WebSockets change
A WebSocket is a persistent, two-way connection between client and
server — opened once, then held open, over which either side can send a
message at any time. Instead of the client repeatedly asking, the server
pushes an update the instant something relevant happens. This is the
foundation under most chat apps, live dashboards, collaborative editors,
and multiplayer features.
In a Rails app, this typically means Action Cable; in a Node.js stack,
Socket.IO or the native ws library — the underlying protocol is the
same either way.
Event-driven architecture is the other half
WebSockets handle delivery; they don’t solve what triggers a message
in the first place. That’s where event-driven design comes in:
something happens in your system (an order is placed, a record is
updated, a job finishes), that event is published, and anything that
cares — including the code pushing WebSocket messages — reacts to it.
This decouples “the thing that happened” from “everyone who needs to know
about it,” which matters as a system grows past a handful of features.
Where real-time features get hard
A few things separate a working prototype from a production real-time
feature:
- Reconnection handling. Networks drop. The client needs to detect a lost connection, reconnect automatically, and reconcile any state it missed while disconnected — not just silently go stale.
- Authorization per-connection, not just per-request. A WebSocket connection is long-lived, so permissions need to be checked at connection time and re-validated if they can change mid-session.
- Horizontal scaling. Once you run more than one server process, broadcasting a message means reaching users connected to other processes too — usually via a shared pub/sub layer (Redis, or a database-backed adapter for smaller deployments).
- Graceful degradation. If the WebSocket connection fails entirely (restrictive corporate networks, for example), the feature should fall back to polling rather than simply not working.
When to reach for this
Real-time infrastructure is worth the added complexity when staleness is
genuinely costly to the user experience — collaboration, live status,
chat. For most other features, a well-cached, fast request-response
endpoint is simpler to build, test, and operate, and users won’t notice
the difference.
Written by
Sarah Chen
Senior Ruby on Rails Engineer
Sarah has spent over a decade building and scaling Rails applications, with a focus on clean architecture, testing, and developer experience.
Related articles
REST vs GraphQL: Choosing the Right API Style for Your Product
Both are proven, production-ready choices. Here's how to decide which one fits your team, your clients, and your data shape.