d8a .tech

Resources / Blog

What Is a Session, Really? GA4, Matomo and Warehouse Sessionization

data tech team • #sessions#sessionization#GA4#Matomo#BigQuery#data-modeling
Feature image

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:

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:

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:

  1. 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.
  2. Waiting. The proto-session stays open while new hits keep arriving.
  3. 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.
  4. Splitting. Rules split it into final sessions where needed.
  5. Writing. Session columns are calculated and written to the warehouse.

The default split rules, all configurable:

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:

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

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.

← Back to Blog

See d8a.tech on your own stack.

Talk to us about your architecture, terms, a partnership or sponsored development. Or start with free Cloud and evaluate it yourself.