Real-time sync is harder than the demo makes it look
Real-time features demo beautifully. Open two browser tabs, type in one, watch it appear in the other — it looks like magic, and the code to do it is often shorter than people expect. That's also exactly why real-time is deceptively hard to ship correctly.
The demo doesn't show what happens when two users edit the same record within the same second. It doesn't show a client reconnecting after a dropped connection with a queue of unsent changes. It doesn't show what the UI should do while it's waiting to find out whether an optimistic update actually succeeded on the server.
Most of the engineering effort in a real-time feature isn't the subscription — it's the reconciliation. Deciding what "true" means when two updates conflict, deciding how long to trust an optimistic UI state before rolling it back, deciding what a user sees during the half-second where the client and server disagree. None of that is visible in a two-tab demo, and all of it is where real products actually break.
We build the conflict cases first, not last. If the reconciliation logic is solid, the happy path is the easy part.
What are the main ways to handle conflicting edits?
- Last write wins. The most recent change overwrites earlier ones. Simple, and fine for settings or fields one person usually owns. Dangerous for shared documents, because someone's work silently disappears.
- Field-level merging. Two people editing different fields of the same record both keep their changes. Only edits to the same field conflict.
- Locking. One person edits at a time and others see the record as read-only. Clear and safe, but frustrating when people need to work together.
- Operational transforms or CRDTs. Edits are merged mathematically so every client ends up with the same result. The right choice for collaborative text and canvases, and the most complex to build. Libraries such as Yjs make it far more practical than writing it yourself.
The right choice depends on the data. Most products use more than one.
What happens when a user goes offline?
A real-time feature has to decide what to do with changes made while disconnected:
- Queue them locally and send them in order when the connection returns.
- Show the user that they are offline, so they know their changes are not yet saved.
- Resolve conflicts on reconnect using the same rules as live edits.
- Handle the case where a change can no longer be applied, such as editing a record someone else deleted.
How should optimistic updates behave?
Optimistic UI shows a change immediately, before the server confirms it. It makes the product feel fast. It also needs a plan for failure:
- Roll the change back clearly if the server rejects it, with a message saying why.
- Never show something as saved that is not.
- Reconcile the local state with the server's version once the response arrives.
How do you test real-time features?
The two-tab demo is the starting point, not the test. Useful checks include simultaneous edits from several clients, artificially slow and dropped connections, reconnecting after long gaps, and many users on the same record at once. Monitoring that shows sync errors in production is part of the feature, not an extra; see observability before launch, not after.
If your product needs real-time features, see custom web app and SaaS development, and read the case for boring architecture for how we keep the rest of the system simple around them.
Building something this applies to?
Book a discovery call→