Designing for sub-100 ms (and why it matters)
Latency is a feature. A look at the architecture choices — realtime fabric, optimistic updates, an offline-capable PWA — that keep every interaction under 100 ms.
By Dmitrii SelikhovFounder
Key takeaways
- There's a threshold around 100 ms below which an interface stops feeling like a website and starts feeling like an instrument, so latency is treated as a feature with an owner, a budget, and a regression test — not something to optimize later.
- Optimistic UI commits every action immediately and reconciles with the server in the background, with CRDTs resolving conflicts where the data structure allows and the rare unresolvable case surfaced as a clear, reversible correction.
- Realtime built as an always-on fabric rather than a bolt-on feature avoids a second data path fighting the first, so one change takes one path and everyone stays consistent in the same frame.
- Fast and offline-tolerant are the same engineering problem: once the client owns a real, queryable copy of the working set, sub-100 ms is the easy case and offline is the edge case of the same architecture, measured from input event to painted frame on real devices.
There's a threshold around 100 ms below which an interface stops feeling like a website and starts feeling like an instrument. Above it, every action has a beat of hesitation that compounds across a workday into genuine drag. You don't consciously notice 200 ms on a single click. You notice it as a vague sense that the tool is heavy, that you'd rather not open it, that doing the thing is slightly more effort than it should be. Multiply that across the hundreds of interactions a day a power user has with their work tool, and latency stops being a technical metric and becomes a question of whether people want to use the product at all.
So we treat latency as a feature with an owner, a budget, and a regression test — not as something we'll optimize later. Later never comes; speed has to be designed in from the data model up.
Optimistic by default
Create an issue, drag a card, post a comment — the UI commits immediately and reconciles with the server in the background. The user never waits on a round trip to see the result of their own action, because the client already knows what the result will be. The server's job is to confirm and to tell everyone else, not to gate the originator's feedback.
The hard part of optimistic UI isn't the happy path — it's being honest about failure. When the rare write loses a race or violates a constraint, you can't just silently drop it or, worse, leave the screen showing a lie. We resolve conflicts with CRDTs where the data structure allows it, so two people editing the same thing converge instead of clobbering, and we surface the rare unresolvable case as a clear, reversible correction rather than a spinner that was never there in the first place. Optimism without a recovery story is just a bug you haven't hit yet.
Realtime as a fabric, not a feature
Presence, cursors, and co-editing run on a realtime layer that's always on, so collaboration never requires a refresh. The same fabric powers live updates across every open view: when an issue changes, every board, list, and timeline showing that issue updates in place, in the same frame, for everyone watching. Realtime isn't a panel you open or a mode you enter. It's the default state of the data.
Building it as a fabric rather than a bolt-on feature is what keeps it fast. A bolted-on realtime feature tends to be a second data path that fights the first one — you fetch over HTTP, then patch over a socket, and the two disagree at the seams. When the live layer is the primary transport for change, there are no seams to disagree at. One change, one path, everyone consistent.
Offline is just fast with the network off
An offline-capable PWA caches the data you're working with, so flaky connections degrade gracefully instead of blocking you. On a train, in a basement conference room, on hotel wifi — the app keeps working, queues your changes locally, and syncs them when the network returns. From the user's seat, offline mode and a bad connection are the same experience: the tool stays responsive and catches up quietly.
What's interesting is that fast and offline-tolerant turn out to be the same engineering problem. Both demand that the client own a real, queryable copy of the working set rather than treating the server as the only source of truth. Once the client can answer most reads from local data and apply most writes locally, sub-100 ms is the easy case and offline is the edge case of the same architecture — not a separate system you build twice.
The discipline this imposes is worth naming: every feature has to be designed so the client can render it without a server round trip in the common case. That constraint sounds limiting, and it is — but it's the same constraint that makes the product feel instant. Speed isn't something you sprinkle on at the end. It's a property of how the data flows, and you either build for it from the first schema decision or you spend years trying to bolt it on.
Measure perceived latency, not server time
The number most teams optimize is the wrong one. Server response time is easy to graph and tells you almost nothing about how the product feels, because the user doesn't experience your p50 — they experience the slowest thing between their click and the screen updating. A 20 ms API call behind 300 ms of client-side rendering, layout thrash, and a blocking re-fetch is a 320 ms interaction, and the user feels every millisecond of it. We instrument from the input event to the painted frame, on real devices on real networks, because that's the latency that exists for a person rather than for a dashboard.
Doing that honestly is humbling. It surfaces the jank that server metrics hide: the dropped frame when a heavy list re-renders, the spinner that flashes for 80 ms and reads as a stutter, the interaction that's fast on the engineer's wired connection and sluggish on a phone three time zones away. You can't fix what you don't measure from the user's seat, and you can't claim sub-100 ms if the only place it's true is your own laptop.
Speed is a budget you defend, not a sprint you win
The thing nobody tells you about a fast product is that it doesn't stay fast on its own. Latency creeps back one reasonable feature at a time — an extra fetch here, a heavier component there, a third-party script someone added for analytics. Each addition is individually defensible and collectively fatal, and six months later the instrument-grade tool feels like a website again. The only defense is a budget with teeth: a hard ceiling on key interactions, enforced in CI, that fails the build when a change pushes a path over the line.
That's the unglamorous truth under all the architecture. CRDTs and realtime fabrics and offline-capable clients are how you earn sub-100 ms; a defended performance budget is how you keep it. Treat speed as a feature with an owner and a regression test and it stays a feature. Treat it as something you'll get back to and it quietly leaves, and your users feel the difference long before any dashboard tells you it's gone.