Ask three analytics tools how many sessions your site had yesterday and you’ll get three different numbers. None of them is broken. They just disagree on what a session is.
That matters more than it sounds. Landing pages, traffic sources, bounce, conversion rate, cost per visit: almost every number marketing looks at is a session-level number. If you don’t know how your sessions are built, you don’t really know what those numbers mean.
A Session Is a Rule, Not a Fact
Visitors don’t announce when a visit starts or ends. A session is a rule someone applied to a stream of events after the fact. Every tool makes the same three decisions:
- When does a session end? Usually after a period of inactivity.
- What else ends it early? A new campaign, a different logged-in user, a day boundary.
- Who calculates it? The browser, the reporting tool, your own SQL, or the pipeline.
Change any of those and your session count changes.
The 30-Minute Convention
The common default is 30 minutes of inactivity. If a visitor does nothing for 30 minutes, the next event starts a new session.
It’s a convention, not a law of nature. A news site where people read one long article might want something longer. A checkout flow with a payment redirect might want to make sure a 20-minute detour to a bank doesn’t split the purchase from the visit that led to it.
How GA4 Does It
GA4 starts a session with a session_start event and stamps every event with a ga_session_id parameter. The default timeout is 30 minutes, and you can adjust it in the admin settings.
Two things changed from Universal Analytics, and they still confuse people comparing old and new numbers:
- Universal Analytics started a new session at midnight. GA4 doesn’t.
- Universal Analytics started a new session when the campaign source changed mid-visit. GA4 doesn’t.
GA4 also adds the engaged session: a session that lasted longer than 10 seconds, had a key event, or had at least two page or screen views.
The Problem: GA4’s Export Has No Session Table
Here’s the part that hurts data teams. GA4’s BigQuery export gives you events. Sessions only exist inside the GA4 interface. In BigQuery, you rebuild them yourself.
Counting yesterday’s sessions looks like this:
SELECT
COUNT(DISTINCT CONCAT(
user_pseudo_id,
CAST((SELECT value.int_value
FROM UNNEST(event_params)
WHERE key = 'ga_session_id') AS STRING)
)) AS sessions
FROM `project.analytics_123456.events_*`
WHERE _TABLE_SUFFIX = '20261018'
That’s just the count. Landing page, session source, session duration and bounce all need more UNNEST, window functions and a pile of edge cases. Most teams end up maintaining a Dataform or dbt project whose main job is to rebuild something GA4 already calculated and didn’t export.
How Matomo Does It
Matomo calls a session a visit. By default, a visit ends after 30 minutes of inactivity, and a new visit starts when the campaign changes. Both behaviors are configurable on a self-hosted instance.
The concept is the same. The details differ just enough that Matomo and GA4 will never report the same visit count for the same site.
How d8a.tech Does It: Sessions on the Backend
d8a.tech builds sessions in the pipeline, before anything reaches your warehouse. Your tables arrive with sessions already done.
Here’s what happens to each incoming hit:
- Grouping. Hits are grouped by identifiers such as the client ID into a proto-session: a set of hits that may form one or more sessions.
- Waiting. The proto-session stays open while new hits keep arriving.
- Closing. After the inactivity timeout passes with no new hits, the proto-session is closed. The default is 30 minutes, and it’s configurable per deployment.
- Splitting. Rules split it into final sessions where needed.
- Writing. Session columns are calculated and written to the warehouse.
The default split rules, all configurable:
- Campaign change: a new UTM campaign starts a new session.
- User change: a different
user_idstarts a new session, so two logged-in people sharing a browser don’t merge. - Time cap: a session is split after 12 hours from its first event.
- Event cap: a session is split after 1,000 events.
You can also choose to join sessions across devices by user_id. A session ID sent by the browser is stored for reference, but the backend decides what a session is.
The result is a set of session_* columns you can query directly: session_id, session_source, session_medium, session_duration, session_entry_page_location, session_exit_page_location, session_total_page_views, session_is_bounced, session_total_purchases, click IDs and more.
The same sessions query becomes:
SELECT
session_source,
session_medium,
COUNT(DISTINCT session_id) AS sessions
FROM events
WHERE date_utc = '2026-10-18'
GROUP BY 1, 2
No UNNEST, no Dataform or dbt model, no rebuilding logic someone else already wrote.
Why Data Lands When the Session Closes
This is the nuance most vendors skip.
A session can’t be complete until it’s over. Its duration, exit page, page count and bounce status depend on events that haven’t happened yet. And the only way to know a visit is over is to wait out the inactivity timeout.
So d8a.tech writes each visit as a whole. When a session closes, all of its events and the finished session columns land in your warehouse at once, instantly. With the default 30-minute timeout:
- A visitor lands at 10:00 and leaves at 10:12.
- At about 10:42, once 30 minutes pass with no new activity, the session closes.
- Its events and session columns are in your warehouse the moment it closes.
That’s not a delay in the pipeline. It’s the definition of a session. The upside: every row you query is final. No session duration that quietly changes after you’ve put it in a report.
We show the timeline on the features page.
What This Means in Practice
- Need data sooner? Shorten the timeout. It’s configurable per deployment. A shorter timeout means data lands sooner, and also more sessions, because shorter pauses start new visits. Compare counts before and after so nobody mistakes a definition change for a traffic change.
- Watching traffic live? Use debug mode. It shows incoming tracking requests as they arrive, before sessions close. See debug mode.
- Every row is final. Reports built on your tables don’t shift after you’ve shared them.
- Don’t expect tools to match. Compare trends, not absolute session counts, when you run two tools side by side.
The Takeaway
Sessions are the backbone of web analytics, and they’re also its most invisible assumption. Know your timeout, know your split rules, and know who calculates them.
If you’d rather receive finished sessions in your own BigQuery or ClickHouse than rebuild them in SQL, start with free Cloud, or read how sessions are built in the code on GitHub.