All posts

Counting Live Visitors Accurately

August 4, 20266 min read

"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