Counting Live Visitors Accurately
"Active now" is a definition, not a fact
There is no objective count of people on your site right now. There is only a definition, and every tool picks a different one.
The common choice is a visitor who generated activity in the last five minutes. Shorter windows undercount people reading a long article; longer ones count people who left.
Whatever you choose, state it in the interface. An unexplained number that disagrees with another tool's unexplained number just erodes trust in both.
Pageviews alone undercount
If the only signal is a pageview, someone reading for ten minutes disappears after five. That is wrong in the most visible way possible — the number drops while the person is still there.
A lightweight heartbeat fixes it:
setInterval(() => {
if (document.visibilityState === 'visible') send({ type: 'heartbeat' });
}, 60_000);One small event per minute per open tab, and only while the tab is actually visible. Backgrounded tabs stop contributing, which is correct — nobody is reading them.
Deduplicate across tabs
Three tabs open is one person. Count distinct visitor ids, not events:
SELECT COUNT(DISTINCT user_id) AS active_visitors FROM events WHERE site_id = ? AND timestamp > now() - INTERVAL 5 MINUTE;
Keep the query cheap
This runs every ten seconds, for every open dashboard. It must stay trivial.
Two things make it so: an index on (site_id, timestamp), and a short cache TTL. Ten seconds of staleness is imperceptible on a live counter and removes almost all of the load — twenty open dashboards become one query instead of twenty.
Active pages, same window
The obvious follow-up is which pages:
SELECT path, COUNT(DISTINCT user_id) AS visitors FROM events WHERE site_id = ? AND timestamp > now() - INTERVAL 5 MINUTE GROUP BY path ORDER BY visitors DESC LIMIT 10;
Use the same window as the headline count. Two different windows produce a page list whose total does not match the number above it, and someone will notice.
Sync lag matters here
In an architecture where events are written to one store and read from another, real-time is where the lag becomes visible. A sixty-second sync interval means "active now" is up to a minute behind.
Either read real-time from the write store directly, or debounce the sync so a burst of events triggers a quick catch-up. The second is usually simpler and keeps one read path.
Try InsightsTrack free
Open-source, self-hosted, privacy-first analytics — explore the live demo, no install required.
Open live dashboard