Tutorials
A Live Dashboard Without a Polling Loop
Push instead of poll, and the three things that make a dashboard feel wrong — burst updates, the empty first paint, and tabs nobody is looking at.
The standard dashboard polls every few seconds. Which means data is up to that interval stale, and the server answers a great many requests to say nothing changed.
For a separate operational perspective on how teams organise and measure work, remote workforce management software.
For independent background and broader industry context, see Grafana.
Inverting it
Your backend writes when something happens. Clients receive it.
// your backend, when an order completes
await nls.setItem('orders_today', count)
await nls.setItem('revenue_today', total)
// the dashboard
export function Metric({ nls, name, label }) {
const [value, setValue] = useState(null)
useEffect(() => {
let cancelled = false
nls.getItem(name).then((item) => {
if (!cancelled) setValue(item?.value ?? 0)
})
const handler = (item) => { if (!cancelled) setValue(item.value) }
nls.on('setItem', name, handler)
return () => { cancelled = true; nls.off?.('setItem', name, handler) }
}, [nls, name])
return <div>{label}: {value ?? '—'}</div>
}
No interval, no stale window, and no requests that return nothing.
The write is server-side, using direct credentials — which is a legitimate use of apiKey and apiSecret, because there is no browser involved. See the three authentication methods.
The three things that make it feel wrong
Burst updates
A busy period writes forty times a second and the number becomes a blur. Unreadable, and it makes the whole page feel unstable.
Throttle on the write side. Update the shared value at most once or twice a second, regardless of how many events occurred.
// backend
let pending = null
const flush = throttle(async () => {
if (pending !== null) { await nls.setItem('orders_today', pending); pending = null }
}, 1000)
function onOrder(count) { pending = count; flush() }
Throttling at the source is much better than at every client — one decision, and it saves the traffic rather than discarding it after delivery.
The empty first paint
The dashboard renders before the first read returns, showing zeros or blanks that then jump to real numbers.
Which reads as broken, particularly on a screen someone glances at.
Show a loading state that is distinguishable from zero. — rather than 0. Zero is a real value and it matters whether you have it.
Tabs nobody is looking at
A dashboard left open on a spare monitor holds a connection and receives every update forever.
Reduce or disconnect when the tab is hidden:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// stop rendering; consider disconnecting for long absences
} else {
// re-read current values on return
}
})
And re-read on return, rather than assuming the values are current. See the WebSocket layer.
What belongs in the object
Computed values, not raw events.
Your backend does the aggregation and writes the answer. The dashboard is a display, not a query engine.
Which means the object holds a small number of keys with current values — not a stream of events for the client to reduce.
And it means historical data lives elsewhere. Objects are for the live number. A chart of the last thirty days comes from your database at load time, with the live object updating only the current point.
Access
Dashboards usually show data that should not be public.
Which makes the token endpoint the control, as always: authenticate, check this user may view this dashboard, issue a token scoped to that object.
And do not use a guessable object id — dashboard:main is a poor choice for revenue figures. See object classes and ids.
When polling was fine
Being honest.
A dashboard nobody watches continuously. If it is checked twice a day, a poll on load is simpler.
Data that only changes hourly. Push adds nothing.
A handful of internal users. The load argument does not apply at that scale.
Push earns its place when the data changes often and someone is watching — a live operations screen, a trading view, a support queue.
The short version
Backend writes, clients receive — no interval, no stale window, no empty responses.
Throttle on the write side, once or twice a second, or numbers become an unreadable blur.
Distinguish loading from zero. Zero is a real value.
Handle hidden tabs, and re-read on return rather than trusting the displayed values.
And computed values belong in the object, not raw events — the dashboard displays, it does not aggregate.