Sessionization: how analytics tools construct sessions
Sessions are computed, not collected. How GA4, Adobe Analytics, and warehouse pipelines each decide where a session begins and ends — and why their counts never reconcile exactly.
last verified 2026-07-11
No beacon on the wire says “session.” A session is a derived construct: the tool receives a stream of timestamped hits per identifier and applies rules to decide where one visit ends and the next begins. Different tools apply different rules to the same stream — which is why “sessions” in GA4, “visits” in Adobe, and “sessions” in your warehouse model are three different numbers, all of them defensible. This guide covers the actual rules, so you can explain the gaps instead of chasing them.
The general algorithm
Every sessionization scheme is a variant of the same procedure:
- Group hits by an identifier (a device cookie, or a stitched person ID).
- Sort by timestamp.
- Start a new session at the first hit, and again whenever a boundary rule fires.
Everything that differs between tools lives in step 3 — the boundary rules — plus two upstream choices that are easy to forget: which identifier defines the grouping, and which timezone defines the timestamps.
GA4: the 30-minute gap
GA4’s model is deliberately minimal. When a user arrives with no active session, the SDK fires a
session_start event and mints a ga_session_id (the Unix timestamp of the session start) plus an
incrementing ga_session_number, both persisted in the _ga_<streamId> cookie and attached as
parameters to every subsequent event.
- Boundary rule: inactivity. A session ends after 30 minutes without an event (default; adjustable per web stream from 5 minutes up to 7 hours 55 minutes).
- That’s the only rule. Unlike its predecessor, GA4 does not end a session at midnight and does not start a new session when the campaign/source changes mid-visit. A GA4 session can span days, and a user who clicks a second ad mid-session stays in the same session (the session’s traffic attribution keeps the first source of the session).
- Counting. A session is a distinct
user_pseudo_id+ga_session_idpair. Standard report surfaces may estimate high-cardinality counts (HyperLogLog++), while the BigQuery export lets you count exactly — so even “GA4 versus GA4” can differ by a small margin between the UI and your own SQL over the export. - Engaged sessions are a filtered subset: sessions with 10+ seconds of foreground time (adjustable), a key event, or 2+ pageviews. Engagement rate replaces the old bounce framing.
Universal Analytics — still the baseline in many teams’ heads — had three boundary rules: 30-minute inactivity, a hard split at midnight in the reporting view’s timezone, and a new session on any campaign change. If a year-over-year comparison spans the UA-to-GA4 migration, those two removed rules alone explain a session-count drop with identical traffic: fewer artificial splits means fewer, longer sessions.
Adobe Analytics: the visit
Adobe’s equivalent unit is the visit, and its default boundary rules are broader. As of mid-2026, Adobe documents a visit as ending when any of these fires:
- 30 minutes of inactivity (the familiar rule);
- 12 hours of continuous activity (a maximum-duration cap);
- 2,500 hits in one visit;
- more than 100 hits in 100 seconds (a bot/runaway-loop heuristic).
The last three exist to terminate pathological streams, but they mean a genuinely long, active session that GA4 reports as one session can be split by Adobe. Adobe likewise does not split a visit on campaign change — but campaign attribution is governed separately by eVar/Marketing Channel expiration settings, so “visits” and “which campaign gets this visit” are independently configurable (a common source of confusion when comparing to GA4, where session and session-attribution logic travel together).
Adobe also lets you redefine the unit itself: Virtual Report Suites with report-time processing support custom visit timeouts and visit-start triggers (“context-aware sessions”), and Customer Journey Analytics defines sessions per data view, at query time, over a stitched person ID. Two Adobe surfaces on the same data can therefore disagree with each other by design.
Warehouse sessionization: you are the vendor now
In a warehouse you implement the boundary rules yourself, typically gap-based windowing over the GA4 export or your own event stream:
with flagged as (
select
user_pseudo_id,
event_timestamp,
case
when lag(event_timestamp) over w is null
or event_timestamp - lag(event_timestamp) over w > 30 * 60 * 1000000 -- 30 min in µs
then 1 else 0
end as is_session_start
from analytics.events
window w as (partition by user_pseudo_id order by event_timestamp)
)
select
*,
sum(is_session_start) over (
partition by user_pseudo_id order by event_timestamp
) as session_index
from flagged;
This is the cleanest definition you will ever own — and it will still not match the vendor UI,
because the vendor applies bot filtering, consent-based modeling, and estimation you are not
replicating. Decide upfront whether your warehouse model should mimic a vendor (use their session
ID where exported, e.g. ga_session_id, rather than re-deriving) or be its own standard.
Why the numbers differ across tools
When GA4, Adobe, and the warehouse disagree, the cause is almost always on this list:
- Different timeout values — someone changed one tool’s timeout and not the others’.
- Different boundary rules — midnight splits (UA legacy), Adobe’s 12-hour/hit caps, campaign-change splits in older tools.
- Different identifiers — a device cookie versus a stitched person ID changes the grouping before any timeout applies; one person on two devices is two cookie-sessions but may be one person-session.
- Timezone — sessionization near day boundaries shifts with the property/report-suite timezone; a warehouse job running in UTC disagrees with a property set to US Pacific.
- Consent and blocking asymmetry — hits lost to blockers or denied consent remove different sessions from different collection paths, and GA4’s behavioral modeling can add modeled sessions the warehouse never sees.
- Cookie lifetime caps — Safari’s ITP caps script-set cookie lifetimes, so returning Safari
users re-appear with fresh identifiers; that inflates users more than sessions, but it also resets
ga_session_numberand shifts new/returning splits. - Bot filtering and estimation — vendors filter known bots before counting and may estimate distinct counts in the UI; raw exports and warehouse models usually do neither by default.
- Late and out-of-order hits — server-side events, Measurement Protocol hits, and offline batches can land after the gap window closed, splitting or extending sessions depending on the processing order. (See how web tracking works for the full pipeline these hits travel.)
Working with it, not against it
- Declare one source of truth per question. Trend analysis and marketing reporting can live in the vendor tool; revenue-grade reporting should live on one documented warehouse definition.
- Reconcile trends, not levels. Two correct implementations will track each other within a few percent and move together; chasing the residual to zero is unbounded work with no payoff.
- Write the definition down — identifier, timeout, extra boundary rules, timezone — next to the dashboards that use it. Most “the numbers are wrong” escalations are two people holding different unstated definitions.
- Remember downstream coupling. Session-scoped attribution and channel grouping inherit whatever sessionization decides; if sessions split differently, attributed conversions split differently too. Sessions are the quiet input to most of the numbers people actually argue about.