A bounce rate needs one bit, not a tracker
Until 28 August this site did not show a bounce rate, and the about page said that was deliberate: it would need a session followed across several page views, and that would need a consent banner.
The first half was true. The second half stopped being true the moment we started recording exit pages, and nobody updated the sentence.
Why it could not simply be calculated
There is a table of page-to-page transitions. It looks like everything a bounce rate needs: a visit that goes from one page to another is not a bounce, so count the visits without transitions.
That does not work, and the reason is in three lines of the recording code. A transition is dropped when the two pages are the same, when more than thirty minutes passed between them, and after three hundred new pairs on one counter in one day. Each rule is right for what the table is for — showing which page leads to which — and each one makes a real second page invisible.
Meanwhile a visit, for the purpose of counting one, lasts an hour: that is how long the row in the live table survives. So the numerator would use one definition of a visit and the denominator another. The number would not have been slightly wrong, it would have been unanchored.
What the data did say
Over 27 and 28 August there were 8,669 visits and 2,817 transitions, so 1.32 pages per visit on average. 99 of 572 counters recorded a transition at all.
That is not a switch being off — path recording defaults to on, and all 166,453 counters have it enabled. Those sites really are single pages: profile pages, one-page sites, a picture gallery on somebody else's platform. A bounce rate for most of them will read close to a hundred per cent, and that will be correct.
One bit, and no new collection
A visit already has a row for an hour. It now carries one more thing: whether this visit ever saw a second, different page. The value is set inside the statement that runs anyway, with no extra query:
ON DUPLICATE KEY UPDATE mehr = mehr | (url <> VALUES(url)),
last_seen = UNIX_TIMESTAMP(), url = VALUES(url)
The order of those assignments is the whole trick. The database evaluates them left to right, so url still holds the previous address when the comparison runs. Put url first and the row compares the new address with itself, the bit stays zero forever, and the bounce rate reads a hundred per cent for everyone. That is a failure that looks exactly like a plausible result, which is why it was worth proving on a throwaway row before trusting it.
When the hour is up, the nightly cleanup counts the expiring rows — how many visits, how many of them multi-page — writes one line per counter and day, and deletes the rows. The bit lives an hour. No cookie, and nothing that outlives it.
What generalizes
The interesting question was never how to track a session. It was which of the things we already write down happens to answer the question, and whether the answer it gives is anchored to the same definition as the question.
Two sources that both look like they measure visits, one with a thirty-minute rule and one with a sixty-minute one, will produce a ratio that is a number and means nothing. It is easier to add one bit to a row that already exists than to explain, later, why the rate is what it is.