Skip to content
Back to blog
GA4Jul 10, 2026 · Ludde Nyström · 14 min read

session_start Missing in GA4: Why It Happens and What It Breaks.

session_start missing in GA4 usually isn't one bug. Consent timing, Measurement Protocol hits, tag sequencing, and timezone bucketing can each cause it.

session_start Missing in GA4: Why It Happens and What It Breaks

session_start is GA4's automatically collected event that opens every session — it fires on its own through gtag.js or the Firebase SDK, mints the ga_session_id and ga_session_number that every other event in that session inherits, and you can't send it yourself even if you wanted to. When it goes missing — page_view and other events keep arriving, but no session_start ever precedes them — GA4 has nothing to anchor that session's traffic source to, and the session lands in "(not set)" or Unassigned instead of just vanishing.

There isn't one root cause. We count at least eight in properties we audit, from consent-mode timing to a GTM tag firing in the wrong order to a timezone quirk that isn't actually a bug at all. Here's how to tell which one you have, why the missing event breaks attribution specifically — not just your session count — and how to confirm it with data instead of guessing.

What session_start Actually Is

It's Reserved — You Can't Send It Yourself, and You Shouldn't Try

session_start belongs to GA4's small set of automatically collected events, alongside first_visit and user_engagement. It's a reserved event name — you cannot fire it manually with gtag('event', 'session_start', …) or a GTM tag. It's generated internally by the tracking library whenever the session-management logic decides a new session has begun, and there's no setting to disable it or trigger it early.

Don't confuse it with first_visit, a separate automatically collected event that fires only once, on a user's genuinely first-ever session. A returning visitor gets a fresh session_start on every new session but never fires first_visit again. If your actual problem is first_visit counts looking wrong rather than session_start itself, you're looking at a related but different issue.

What Triggers It

Per Google's own definition, a session starts "when a user opens your app in the foreground or views a page or screen and no session is currently active." The first hit gtag.js or the Firebase SDK sends after that moment tries to attach itself to an active session; if there isn't one, the library creates one and fires session_start alongside it. That automatic session association is a property of the standard client libraries specifically — it does not extend to events sent through the Measurement Protocol or Data Import. Hold onto that distinction; it's the root of cause #2 below.

Why a Missing session_start Is an Attribution Bug, Not Just a Counting One

This is the part most troubleshooting threads skip past: session_start isn't just a counter that increments Sessions. It's the event GA4 uses to decide, at the moment a session opens, what traffic source gets credit for it — reading whatever UTM parameters or referrer were present on that first hit. When session_start never fires for a given ga_session_id, GA4 still has events tagged with that session ID, but it never ran the attribution logic that would assign them a source and medium. "Session source / medium" comes back (not set) — not wrong, not blank by coincidence, but genuinely never populated.

Worth being precise, because "(not set)" and "Unassigned" get used interchangeably and aren't the same failure. (not set) means the dimension itself has no value — nothing was captured for source/medium on that row. Unassigned means GA4 did get a source/medium value, but it didn't match any rule in your Default Channel Grouping. A missing session_start produces the first one. If you're actually seeing Unassigned traffic with real-looking source/medium values attached, session_start is firing fine and the problem is in the UTM values or channel-grouping rules themselves — see our breakdown of why UTM parameters stop showing up in GA4. And if the gap only appeared after a container change — a new tag, a resequenced trigger — check whether this is a GA4 problem at all or a bug living in the handoff between GA4 and GTM, which is exactly what our piece on the bugs that live between GA4 and GTM covers.

The 8 Root Causes We See in GA4 and GTM Audits

1. Consent Mode Defaulting to Denied Before the First Event

Consent Mode requires strict ordering: the gtag('consent', 'default', …) command (or GTM's Consent Initialization trigger) has to run and set analytics_storage to its default state before the GA4 tag reads it — if the config tag reads consent state first, it reads an undefined state and can behave inconsistently. The more common version of this problem: a visitor lands with consent genuinely denied, GA4 sends a reduced-data "cookieless ping" rather than a full hit, and that ping doesn't carry the identifiers — ga_session_id among them — that a session needs to route through GA4's normal session-management flow the same way a fully-consented hit does.

Google introduced backend improvements in late 2024 aimed at retroactively reattaching session context to events that fired before a visitor granted consent, provided the follow-up consent-update command fires correctly — but Google has kept iterating on how Advanced Consent Mode reconciles pre-consent activity, so confirm current behavior on your own property rather than assuming. To verify: open DebugView in a fresh incognito session with the consent banner still showing, and confirm the Consent Initialization tag (or the default command) is the very first thing on the timeline, ahead of the GA4 configuration tag.

2. Measurement Protocol Hits That Mint a Session Without Ever Sending session_start

This is the cleanest, most structural cause on the list — less a bug than a consequence of how the Measurement Protocol works. Sending an event with a session_id GA4 hasn't seen before is enough on its own to open a new session; you don't send session_start to make that happen, and, per the distinction in cause #1, the automatic session-association logic that generates session_start only lives in the client libraries, not in server-to-server calls. Any session that originates from a server-side Measurement Protocol hit — a CRM-confirmed purchase, a backend-triggered signup, a custom CDP integration — is, by construction, a session with events but no session_start row anywhere.

The fix isn't always "make session_start appear" — often it's deciding whether that server-side traffic should carry session-level attribution at all. If it should, don't mint a fresh session_id server-side; carry the client_id and session_id from the browser session that originated the interaction into your server-side call, so the event attaches to a session GA4 already has attribution context for instead of opening a new, contextless one. If it shouldn't — a lot of backend-confirmed events genuinely don't need session-level attribution — leave it as-is and stop treating the missing session_start as a defect.

3. A GTM Event Tag Firing Before the GA4 Configuration Tag

Every GA4 event tag in GTM depends on its associated Google tag (the GA4 Configuration tag) having already initialized on the page. If a Custom Event tag fires first — a fast-firing custom trigger, or a tag with a higher firing priority than the config tag — it sends without the session context the configuration tag would otherwise establish, producing either an incomplete session or the same downstream "(not set)" source/medium symptom described above.

The fix: give the GA4 Configuration tag a higher firing priority than every GA4 Event tag in the container, or use GTM's Tag Sequencing ("Fire a tag before this tag fires") to force event tags to wait for it explicitly. Confirm the actual request order afterward in the browser Network tab — the request carrying the config hit should always leave before any event-tag request on a fresh page load. This is a close cousin of the sequencing bugs covered in our guide to GTM tags that won't fire.

4. Deferred or Lazy-Loaded GTM/gtag.js for Page-Speed Reasons

Delay-loading the GTM or gtag.js snippet by a few seconds — or until first scroll or click — is a common Core Web Vitals fix, and it usually doesn't break session_start outright: the tag still fires once the delayed load happens, just later than the real page load. The failure mode is subtler: if a client-side redirect, an SPA route change, or a UTM-stripping rewrite happens in that gap, whatever landing-page URL and referrer gtag.js reads once it finally loads may no longer be the one the visitor arrived on. session_start still fires — just against a scrubbed context — and the report-level symptom looks identical to a genuinely missing event: no clean source/medium for the session.

The fix: if you delay-load for performance, capture the landing-page URL and document referrer into a first-party variable at the actual moment the page loads — before the delay — and feed that captured value into GTM once it finally boots, instead of letting the delayed script read window.location fresh at whatever point it happens to initialize.

5. Session Timeout Edge Cases

The standard client libraries re-evaluate session state on every hit, so a genuine 30-minute-plus idle gap doesn't need a full page reload to produce a fresh session_start — the next real interaction after the gap triggers it automatically. The actual break case isn't the timeout itself; it's a custom event pipeline that bypasses the configuration tag's session logic entirely, most often a raw fetch() call to the Measurement Protocol endpoint hand-rolled inside a single-page app — structurally the same root cause as #2, just triggered by an idle gap instead of a backend integration. For the full mechanics of how the timeout and engaged-session settings interact, see our GA4 session timeout settings guide.

6. Midnight and Timezone — a Myth and a Real Gotcha

The myth first, because it's widely repeated: GA4 does not end a session at midnight the way Universal Analytics did. A session that starts at 11:55 PM in your property's reporting timezone and continues past midnight stays one continuous session — GA4 dropped the arbitrary daily reset. If you're troubleshooting on the assumption that midnight forces a new session_start, that assumption is the bug.

The real gotcha is more subtle. Every event — session_start included — is date-stamped using whatever timezone your property is configured with (Admin → Property Settings → Reporting time zone). A session that opens at 23:58 gets a session_start row dated the earlier calendar day, while later events in that same unbroken session land in the next day's tables. Filter a report or Exploration to a single day, and a session that began the day before but had activity into your selected day can look like it has no session_start for that day — the row isn't missing, it's dated the day before. This gets worse if your property's timezone doesn't match your audience's actual local time, and it isn't retroactive: changing it only affects data collected going forward.

7. Bot Filtering and Internal Traffic Filters

GA4's built-in bot filter — "Exclude all hits from known bots and spiders" — is on by default for every property, can't be turned off through the standard UI, and matches against a known-bot user-agent list Google maintains (based on the IAB/ABC International Spiders and Bots List). A matching hit gets dropped before it's processed, session_start included — but only for traffic that self-identifies as a bot in its user agent. It won't explain a gap in genuine human sessions.

Internal traffic is a two-step trap. Defining internal traffic rules (Admin → Data Streams → [your stream] → Configure tag settings → Show all → Define internal traffic, by IP or CIDR range) only tags matching hits with traffic_type = internal — it removes nothing by itself. Removal happens only if you also create a Data Filter at Admin → Data Settings → Data Filters, set to Active with an Exclude operation. Testing mode is harmless; an Active filter silently drops every matching session — session_start included — for anyone on that IP range, including a remote employee, a VPN exit node, or an office whose IP changed after a router swap. See our walkthrough on setting up internal traffic filters correctly if you're not sure which state yours is in.

8. Reporting Lag Masquerading as a Missing Event

Standard GA4 reports can take up to 24-48 hours to fully process. If you're checking a campaign or a fresh deployment the same morning it went live, an apparently missing session_start in the standard reports is very often just processing lag, not a bug. Confirm with Realtime or DebugView before you assume anything is actually broken — both confirmation methods below start there for exactly this reason.

How to Confirm session_start Is Actually Missing

Start with Realtime or DebugView

Before touching a standard report, rule out processing lag directly: open Reports → Realtime and generate a fresh test session, or connect the Google Analytics Debugger extension and watch Admin → DebugView. If session_start doesn't appear on your own controlled test visit, you have a live, reproducible bug you can iterate on immediately — reload, tweak, reload again — rather than waiting on a report that won't finish processing until tomorrow.

Compare Event Count Against Sessions in Explorations

Go to Explore → Free form, build a table with Event name as the row dimension and Event count as a metric, add Sessions as a second metric, and compare the session_start row's Event count against the total Sessions figure for the same date range. In a healthy property the two numbers track closely — they won't match exactly, since Sessions is a session-scoped metric with its own counting rules, but they shouldn't diverge by a wide, obvious margin.

Run this over a multi-day range rather than a single day — a full week is usually wide enough to smooth over the midnight date-bucketing effect described above, which can make even a perfectly healthy property look like it's missing events when you filter to just one day. If session_start's count sits meaningfully below Sessions across a full week, you're looking at a real gap worth chasing down, not a timezone artifact.

BigQuery: Find the Exact Sessions Missing session_start

If you have BigQuery export linked, this is the most precise version of the check, because you're working with unsampled, event-level rows instead of a UI-aggregated metric. This query groups every event by its ga_session_id and surfaces any session that has events but zero session_start rows in the date range you scan:

WITH session_events AS (
  SELECT
    (SELECT value.int_value FROM UNNEST(event_params)
     WHERE key = 'ga_session_id') AS session_id,
    event_name
  FROM `your-project.analytics_XXXXXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260701' AND '20260707'
)
SELECT
  session_id,
  COUNT(*) AS total_events,
  COUNTIF(event_name = 'session_start') AS session_start_count
FROM session_events
WHERE session_id IS NOT NULL
GROUP BY session_id
HAVING session_start_count = 0
ORDER BY total_events DESC;

Every row that comes back is a real ga_session_id GA4 tracked events under, with genuinely zero session_start rows in the scanned range — not a reporting-lag artifact, an actual gap. Pull a handful of those session IDs and check their other event parameters (page_location, traffic_source, engagement_time_msec) for a pattern: do they cluster around an hour that lines up with a GTM publish, share a traffic source pointing at server-side delivery, or correlate with a specific consent state? That pattern is usually the fastest route from "confirmed bug" to knowing which of the eight causes above it actually is.

page_view Firing Without session_start? We Already Check for That

One of the checks in NiceLookingData's GA4 audit — "Missing Core Events" — looks specifically for properties where page_view is arriving but session_start and/or user_engagement aren't, and flags it CRITICAL, because session and engagement metrics can't be calculated without them. Run a free GA4 audit to see whether your property has the gap before you go digging through BigQuery.

Frequently Asked Questions

Why is session_start missing in GA4?

The most common causes are: Consent Mode defaulting to denied and not being properly reattached to session context after consent is granted; events arriving through the Measurement Protocol with a fresh session_id, which opens a session without ever sending session_start (this happens because the automatic session_start behavior belongs specifically to client libraries like gtag.js, not to server-to-server calls); a GTM event tag firing before the GA4 configuration tag due to trigger or priority ordering; and an internal-traffic Data Filter left Active with an Exclude operation, silently dropping every matching session. Less commonly, what looks like a missing event is actually normal 24-48 hour reporting lag, or a session that genuinely fired session_start but got date-stamped on the previous calendar day due to your property's timezone setting. Confirm with DebugView or a BigQuery query before assuming any specific cause.

Can I manually send the session_start event in GA4?

No. session_start is a reserved, automatically collected event name — GA4 treats it as internally generated by the standard client libraries whenever their session-management logic determines a new session has begun, and there is no supported way to trigger it manually through gtag.js, a GTM tag, or the Measurement Protocol. If you need session-level attribution on server-side or Measurement Protocol traffic, the correct approach is to reuse the client_id and session_id from an existing browser-initiated session rather than trying to synthesize session_start yourself.

Does GA4 split sessions at midnight?

No — this is a holdover assumption from Universal Analytics, which did force a session split at midnight. GA4 dropped that behavior; a session that starts before midnight and continues past it in your property's reporting timezone stays a single, unbroken session. What does change at midnight is which calendar day individual events get date-stamped into for reporting purposes — a session_start that fires at 11:58 PM is dated the earlier day even though later events in that same session land in the next day's tables. That's a reporting-date quirk, not a session split, and it's the most common reason session_start looks "missing" when you filter a report down to a single day.

How do I check if session_start is actually firing?

Start with DebugView (Admin → DebugView, paired with the Google Analytics Debugger extension) on a fresh test session — if session_start doesn't appear there, you have a live, reproducible bug you can debug in real time. For a property-wide check, use Explorations: build a free-form table with Event name as the dimension and Event count plus Sessions as metrics, and compare the session_start row against total Sessions over a full week rather than a single day, to avoid the midnight date-bucketing artifact described above. For the most precise answer, query your BigQuery export for ga_session_id values that have events but zero session_start rows in the date range — that result set is your actual list of affected sessions, not an approximation.

Written by
Ludde Nyström — Founder, NiceLookingData

Analytics consultant turned founder. After years running the same GA4 and GTM audits across client engagements, Ludde built the audit into a product — so the pattern-matching takes a minute, not a meeting. More about Ludde →

Free tool

Run a free GA4 audit.

Connect your Google Analytics 4 property. Our auditor runs 61 checks and gives you an instant health score with a plain-English action plan.

Thanks for reading.