Both are stopwatch numbers, and both have a hole in the same place: the last thing a visitor does. Here is how each is computed, what goes missing, and how to read time without fooling yourself.
Time on page is how long a single page was in front of the reader. Session duration is the span of a whole visit across every page in it. The first can be recorded by the page itself. The second needs a rule for where a visit begins and ends, and that rule is where tools disagree.
Time on page answers a narrow question: for how long was this page open in front of someone? It belongs to one pageview. If a visitor reads three pages, there are three time-on-page values, one per page, and no single number for the visit.
There are two ways to get that value. The old way is to subtract timestamps: when a visitor loads page B, the tool assumes they left page A at that moment, so A's time is the gap between the two pageviews. The newer way is to have the page report its own elapsed time at the moment it is hidden or closed. The first needs a next pageview to exist. The second needs a message to arrive after the visitor has already stopped looking.
Both approaches measure presence, not attention. Neither knows whether the reader was reading, skimming, on a call or making coffee.
In the timestamp-gap model, the last page of a visit has no successor, so there is nothing to subtract. It records zero or is left out. A visitor who lands on one article and reads it for six minutes looks identical to one who landed and left after a second. This is the best-known blind spot in classic web analytics, and it is why a one-page visit in a hit-based tool shows no duration at all.
The leave-beacon model fixes this for the common case, because the last page reports its own time. It trades one problem for another: the message is sent while the page is closing, and a tab killed by the browser, a crashed process or a dead network connection can lose it. Some pageviews end with no time value, and a good tool leaves them out of the average rather than counting them as zero.
Session duration is the elapsed time of a visit: from its first interaction to its last. Everything hard about it is in the word "visit". Cookie-based tools typically define a visit with an identifier plus an inactivity timeout, and the duration is the time between the first and last recorded activity inside that window. Change the timeout and the duration changes, with no change in visitor behavior.
Then the last-page problem applies again, now to the whole visit. If duration is last activity minus first activity, the time spent on the final page is never added, because nothing marks its end. A visit made of one long read has a duration of zero. A visit of two quick pageviews followed by a ten-minute read on the last page shows a duration of seconds. Session duration therefore tends to undercount, and it undercounts most on content sites where the visitor's longest stay is the page they leave from.
Averages make this worse. Session duration averages usually fold in the zero-length one-page visits, which pulls the mean down, or exclude them, which makes it a statistic about multi-page visitors only. Two tools can both report "average session duration" and be describing different populations.
GA4 moved away from the timestamp-gap approach. It reports engagement time, built from the time a page or app screen was in the foreground rather than the gap between hits, which lets it count time on a final page. That is a genuine improvement on Universal Analytics, and it is one of the places where GA4 is the better instrument. The cost is the same as always: cookies by default, a consent banner in the EU and UK, and a heavy tag. Check the current Google Analytics documentation for the exact definitions, since Google adjusts its reports; our GA4 alternative guide covers the trade-offs.
The mechanics are short enough to state completely.
gm.js stores the current time. The pageview beacon goes out immediately, and it carries no time value yet.beforeunload. Whichever fires first sends a leave beacon with the elapsed time in whole seconds. A latch ensures one leave beacon per page: later hides on the same page do not send a second value.pushState or popstate navigation sends a leave for the route being left, then starts a fresh clock for the new one. A click on an in-page #anchor link also fires popstate in browsers, so on any site it ends the current measurement and starts a new pageview on the same path.Two consequences are worth stating before you read a number.
The clock stops at the first hide, not the last. A reader who switches tabs after 20 seconds and returns to read for four minutes is recorded as 20 seconds. On phones, switching to another app typically hides the page, so a reader who flips away mid-article and comes back has the same effect. Time on page is closer to "time until the reader first looked away" than to total reading time.
The clock runs while the tab is open and visible, or simply open. The tracker uses wall-clock seconds since it started, with no idle detection. A tab left in the foreground over lunch is recorded as a long visit until it is hidden.
All of these are on the free plan:
It does not report one, and the design explains why. A session in GhostMetrics is one browser tab. The tracker keeps a pageview counter in sessionStorage, cleared when the tab closes, with no cookies, no visitor IDs and no inactivity timeout. A duration would need a start time and an end time for that tab-session, and the product does not store either as a visit-level value. Each stored time belongs to one pageview.
That is a deliberate trade, and it has a cost you should weigh. If you report "average visit length" to a client or a board, GhostMetrics will not supply it. What you can build from the free export is a per-page picture: sum or average time_on_page by page, source or campaign, using only rows where is_exit is 1, as the dashboard does. What you cannot build is a visitor-level timeline, because no identifier links one session to the next.
The same constraint applies to returning visitors, per-visitor funnels and cross-session attribution. If those are requirements, GA4 or another identifier-based tool is the right instrument; our Google Analytics alternatives roundup lays out the field.
| Question | Timestamp-gap tools | GA4 | GhostMetrics |
|---|---|---|---|
| Unit of time | Gap between consecutive pageviews | Engagement time, from foreground time | Seconds from tracker start to first hide or unload |
| Last page of a visit | Not measured; one-page visit reads as zero | Measured as engagement time | Measured when the leave beacon arrives |
| Session duration figure | Yes, last activity minus first | Engagement-based figures; check current docs | None |
| Idle or unattended tab | Counted until the next hit or timeout | Foreground-based; check current docs for edge cases | Counted until the tab is first hidden |
| Missing data | Zero or omitted | Model-dependent | Skipped in averages, never counted as 0 seconds |
| Identifier needed | Cookie or similar | Cookie by default | None; tab-scoped counter only |
The figures below are illustrative, not benchmarks. Suppose a blog post gets 10 pageviews in a week, and leave beacons arrive for 6 of them. The measured times are 8, 12, 40, 95, 180 and 420 seconds. The other four tabs were closed by the browser or lost their connection before a beacon could send.
The dashboard averages the six measured rows: the sum is 755 seconds, so Avg Time reads 2m 6s. It does not divide by ten. The median of the same six values is 67.5 seconds, a figure the dashboard does not show, and the gap between the two is the point. One 420-second row, which could be a reader or could be an abandoned tab, moves the mean from 67 seconds without it to 126 seconds with it.
The distribution tells the honest story. The dashboard's distribution panel is site-wide, so bucket this page's six rows from the export the same way. Of the six measured pageviews, one is under 10 seconds, one is 10 to 30 seconds, two are 30 seconds to 2 minutes, one is 2 to 5 minutes and one is over 5 minutes. That is a spread, not a typical reader, and it is why this guide keeps pointing at the distribution rather than the average.
One more thing the example shows: the four missing beacons are not zeros, and they are not evenly distributed either. Pageviews that end in a crash or a dropped connection are not a random sample of readers, so treat any average as a description of the visits that could be measured.
data-gm-event on the button that matters. The custom events guide shows the setup.Presence is not attention. A long time on page is compatible with a delighted reader and with a forgotten tab. Use it as a shape, and let scroll depth and counted actions do the work of telling the two apart. Our guide to scroll depth tracking covers the companion signal.
A timestamp-gap average, a GA4 engagement-time figure and a leave-beacon average describe different things. If you migrate, expect the headline number to jump on day one and write the date next to the chart. Run both tools in parallel for a few weeks and note the offset instead of reading it as a change in your audience.
The cheapest ways to raise average time are to slow the page, bury the answer or add a modal the reader must dismiss. None of these helps anyone. If a change improves the number and worsens the page, the number stopped measuring what you wanted. Real-user load timings belong in the vitals panel; see real-user Core Web Vitals monitoring. GhostMetrics captures FCP, LCP, CLS and TTFB and does not capture INP.
Bounce rate counts single-page sessions, and time on page measures duration. A one-page visit can be 4 seconds or 4 minutes, which is why the two metrics are best read together. The full comparison is in bounce rate vs engagement rate explained.
Spikes of very short visits from one referrer can be a link preview, a scraper or a misconfigured campaign link. Check the Referrers panel for the dates in question before rewriting a page.
| Question | Look at | Why |
|---|---|---|
| Do people read this article? | Its Avg Time and Avg Scroll in All Pages, plus its rows in the export | The distribution panel is site-wide, so one page's shape comes from bucketing its exported rows |
| Where does reader time go across the site? | Attention Minutes by page | A sum shows total time, not per-visit time |
| Do visitors go past the first page? | Bounce rate and Pages / Session | A page count needs no timing assumptions |
| Is a landing page failing fast? | Its Avg Time and Bounce Rate in All Pages, plus the click event | Very short stays plus single-page sessions point to a page that does not hold the visitor |
| How long is a typical visit? | GA4 or another identifier-based tool | GhostMetrics stores time per pageview, not per visit |
| Is the page slow to arrive? | Its row in the per-page vitals table | Timing problems show up in load metrics before they show up in time on page |
The short version: time on page tells you how long a page held someone until they first looked away, and session duration tells you, at best, how long a visit lasted without its final page. Neither says whether the visitor was satisfied. Combine a time distribution with scroll depth and a counted action, and you are as close as page-level analytics gets. For the wider routine, our weekly dashboard guide shows where these panels fit.
GhostMetrics puts average time on page, a time-on-page distribution, attention minutes, scroll depth and session depth side by side, so a long or short stay is one click from the evidence that explains it. It sets no cookies, stores no IP addresses (they are hashed) and keeps no cross-session identifier. Install is one script tag in <head>, about 3 KB compressed on the wire. It has no session duration figure; if you need one, use GA4.
The free plan is free forever, no card: one website, unlimited pageviews, a real-time dashboard, every view and full history. Raw event export as CSV or JSON is on every plan, including free. Pro is $9.99/mo flat with unlimited websites (fair-use ceiling of 250) and a public shareable dashboard, and it starts with a 30-day free trial that needs a card and charges nothing for 30 days.
Start free, no card See the live demoDisclosure: GhostMetrics is built by Null Agency and we use it on this site, so we are not a neutral party about our own product. GA4's definitions are set by Google and change over time; check the current Google Analytics documentation before quoting exact figures. The numbers in the worked example are illustrative, not benchmarks.