Every dashboard you build makes an implicit promise: the number on the screen is true right now. Break that promise quietly and you create a very specific kind of bug — the one where everything looks fine until it very much isn't.
I work on industrial dispatching systems: SCADA and building-automation software that shows operators the live state of boilers, ventilation and pumps. But the lesson I want to share isn't industrial at all. It applies to any UI that displays data from a source it doesn't fully control — which is almost every dashboard, admin panel and monitoring view ever shipped.
The last-known-value trap
Here's the failure mode. Your frontend subscribes to some value — a temperature, a queue depth, a server's health, a price. It renders the latest value it received. Then the source goes quiet: the device drops off the network, the websocket silently dies, the polling job wedges, the upstream API starts timing out.
What does the screen show? The last value it got. Confidently. With no visual difference between "this is live" and "this is a fossil from twenty minutes ago."
In our world that means an operator sees a calm 21 °C for a room that is actually freezing, because the sensor stopped reporting an hour ago and nobody told the UI. The operator trusts the screen, so they trust the stale number, so they don't act. The incident report afterwards always contains the same sentence: "but the screen said it was fine."
You have shipped this bug. I have shipped this bug. It is one of the most common and least-discussed defects in data-facing software, because it only appears when something else breaks — and by then everyone is looking at the other thing.
Why it's so easy to miss
- It tests green. In dev and staging, data flows constantly. The stale state only happens when a real source fails in production, which your tests rarely simulate.
- The "happy path" rendering is the same code path. Showing the last value is literally the default behavior of every reactive binding. You have to add work to make staleness visible — so by default, it's invisible.
- Absence of data is not an event. Your code reacts to messages arriving. Nothing arriving is, by definition, nothing to react to. You have to actively go looking for silence.
The fix: every value carries a freshness budget
The pattern that has never failed us is simple: a value is not just a number, it's a number plus the time it was observed. Every value gets a freshness budget — roughly how long it's allowed to go without an update before you stop believing it.
{ value: 21.4, unit: "°C", observedAt: 1718658000, maxAgeSec: 60 }
Then a single derived check, evaluated on render (or on a timer):
const isStale = (now - observedAt) > maxAgeSec;
If it's stale, the UI must say so: grey the value out, add a "last updated 14 min ago" line, drop a warning badge — anything that breaks the visual promise of liveness. The exact treatment matters less than the principle: a value you can no longer vouch for must not look identical to one you can.
The budget should be tied to how often the source is supposed to update. A sensor polled every 5 seconds that's been silent for 3 minutes is clearly dead. A daily batch metric silent for 3 minutes is perfectly healthy. Hardcoding one global timeout gets both wrong — derive the budget from the expected cadence, and clamp it to sane bounds so a misconfigured source can't make the gate useless.
Where to put the check
Resist the urge to bury this in each component. Staleness is a property of the data, not the widget, so compute it as close to the data layer as you can — in the store, the selector, the query hook — and let every consumer inherit it. A new screen that displays the same value then gets honesty for free, instead of re-implementing (and forgetting) the check.
It also composes nicely with something you probably already have: connection state. "Socket disconnected" is a coarse, global signal; per-value freshness is the fine-grained one. You want both. The socket can be perfectly healthy while one specific device behind it has gone dark.
The broader principle
This is really a special case of a rule worth tattooing somewhere: a UI should never present a guess as a fact. Stale data, optimistic updates that were never confirmed, cached values past their useful life, the spinner that spins forever after the request already failed — they're all the same sin. The interface is asserting something it can't currently back up.
The fix is always the same shape: track the provenance and freshness of what you show, and when you can't vouch for it, look like you can't. Honest uncertainty beats confident fiction every time, because users — operators, admins, customers — calibrate their trust on what the screen looks like, not on the messy truth underneath it.
We learned this the expensive way, watching real plants where a frozen number on a screen meant a real pipe could freeze for real. But you don't need a boiler room to benefit from it. Any time you render data you didn't generate this instant, ask the boring question: how do I know this is still true — and what does the screen do when it isn't?
I build industrial automation and dispatching software at Atlas Scada. The freezing-room example is real; the fix above is the one we ship.
Top comments (0)