GA4 11 min read

GA4 Zero Traffic Bug: How to Detect Data Loss Incidents and Backfill Missing Sessions from BigQuery

On September 1, GA4 properties across the web showed zero traffic for hours — and most teams didn't notice until clients asked why reports were empty. If your monitoring depends on the GA4 UI, you're

A
Ashwani Bhasin
·

On September 1, GA4 properties across the web showed zero traffic for hours, and most teams didn’t notice until clients asked why reports were empty. If your monitoring depends on the GA4 UI, you’re already too late. By the time the Realtime report is visibly broken, you’ve lost the window to triage, communicate, and start planning the backfill.

I’ve handled three of these incidents in the past eighteen months for ecommerce and SaaS clients. The pattern is always the same: Google’s status dashboard stays green for hours, the GA4 UI shows partial or zero data with no error state, and the client’s marketing team is the first to raise the alarm. That’s a bad position to be in. This post walks through the detection stack we run internally, how we tell the difference between collection failure and processing lag, and the exact process we use to reconstruct missing sessions and revenue from BigQuery, server-side GTM, and Shopify order data.

Why the GA4 UI Is the Wrong Place to Detect Data Loss

The Realtime report feels like it should be your canary. It isn’t. Here’s why.

Realtime shows events from the last 30 minutes, but it’s a separate pipeline from the one that populates standard reports. During the September incident, Realtime showed traffic for some properties while standard reports showed nothing. In another incident last year, Realtime went dark but data still landed in BigQuery the next day. The two systems fail independently.

Standard reports have a documented processing latency of 24 to 48 hours for full data availability. Google states that “most” data appears within 4 hours, but that word does a lot of work. During any processing lag event, the report can look normal for the previous day and empty for today, which is indistinguishable from a normal Tuesday morning at 9 AM when yesterday’s numbers are still settling.

The trap: if you check GA4 at 10 AM and see yesterday’s data looking healthy, you have no signal about whether today’s collection pipeline is working. You’ll find out tomorrow, or when a client emails you.

The only reliable signal is the BigQuery export. If you have GA4 linked to BigQuery (and if you don’t, fix that first — it’s free up to 1 million events per day and the intraday table gives you near-realtime access), you can query the raw event stream and compare it against historical baselines. That’s what our monitoring is built on.

Building a Cheap Anomaly Detector in BigQuery

The goal: get a Slack ping within 15 minutes when hourly event volume drops significantly below expected levels. No fancy ML, no third-party tools, just a scheduled query and a webhook.

The intraday table events_intraday_YYYYMMDD updates continuously through the day. It has slightly different schema quirks from the daily table (some fields are populated later), but for counting events by hour it’s fine.

Here’s the core detection query. It compares the current hour’s event count against the same hour across the previous 7 days and flags anything below 50% of the rolling median.

DECLARE current_hour INT64 DEFAULT EXTRACT(HOUR FROM CURRENT_TIMESTAMP());
DECLARE today STRING DEFAULT FORMAT_DATE('%Y%m%d', CURRENT_DATE());

WITH current_volume AS (
  SELECT
    EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp)) AS hour,
    COUNT(*) AS event_count
  FROM `your-project.analytics_XXXXX.events_intraday_*`
  WHERE _TABLE_SUFFIX = today
    AND EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp)) = current_hour - 1
  GROUP BY hour
),
baseline AS (
  SELECT
    EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp)) AS hour,
    APPROX_QUANTILES(daily_count, 100)[OFFSET(50)] AS median_count,
    APPROX_QUANTILES(daily_count, 100)[OFFSET(20)] AS p20_count
  FROM (
    SELECT
      event_date,
      EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp)) AS hour,
      COUNT(*) AS daily_count
    FROM `your-project.analytics_XXXXX.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 8 DAY))
      AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
    GROUP BY event_date, hour
  )
  WHERE hour = current_hour - 1
  GROUP BY hour
)
SELECT
  c.hour,
  c.event_count AS actual,
  b.median_count AS expected,
  b.p20_count AS lower_bound,
  ROUND(c.event_count / b.median_count * 100, 1) AS pct_of_median,
  CASE
    WHEN c.event_count < b.median_count * 0.3 THEN 'CRITICAL'
    WHEN c.event_count < b.p20_count THEN 'WARNING'
    ELSE 'OK'
  END AS status
FROM current_volume c
CROSS JOIN baseline b;

Schedule this as a BigQuery scheduled query to run every 15 minutes past the hour. Wire the output to a Cloud Function that posts to Slack when status is WARNING or CRITICAL. Total infrastructure cost for a mid-size property: under $5/month.

A few practitioner notes on this query that most guides get wrong:

Don’t alert on the current in-progress hour. The intraday table lags a few minutes and you’ll get false positives at the top of every hour. Always compare the previously completed hour.

Use a percentile-based lower bound, not standard deviation. GA4 event volume isn’t normally distributed. There are big spikes around campaign launches and quiet weekends. Median plus 20th percentile is more forgiving of natural variance.

Segment by event type for high-value properties. For ecommerce clients we run separate detectors for purchase, add_to_cart, and page_view. A collection failure often hits some tag configurations before others.

The intraday table breaks when it breaks. If GA4’s collection pipeline fails, the intraday table itself may stop updating. Add a secondary check: if the max event_timestamp in the intraday table is more than 30 minutes old, that’s its own alert.

The Three Failure Modes and How to Tell Them Apart

Every GA4 incident falls into one of three buckets. Treating them the same is a mistake because the recovery path is different for each.

Failure ModeWhat HappensBigQuery SignalRecoverable?
Collection failureHits never reach GA4 serversIntraday table has gaps or stops updating; server-side GTM logs may show 5xx from GA4 endpointNo — data is lost unless you have server-side logs
Processing lagHits collected but not yet in reportsIntraday table populates normally; standard reports emptyYes — data appears within 24-72 hours
Reporting lagData processed but UI/API returns nothingBigQuery daily export contains data; UI shows zeroYes — usually resolves within hours

The diagnostic sequence:

  1. Query the intraday table for the last completed hour. If it has roughly normal volume, collection is working. Move to step 2.
  2. Query the previous day’s events_ table. If the daily table exists and has expected volume, processing is working. The issue is reporting lag.
  3. If the daily table is empty or missing for a day that should have been processed by now, you have processing lag. Data will likely arrive, but stakeholder communication needs to happen now.
  4. If the intraday table is empty or heavily degraded, you have a collection failure. This is the worst case and the one where backfill matters.

For the September incident, we saw collection failure for approximately 4 hours across affected properties, followed by processing lag as Google worked through the backlog. Some data eventually appeared. Some didn’t. That’s why you need a backfill plan.

Backfilling Missing Data From BigQuery, sGTM, and Shopify

Here’s the honest part: you cannot fully reconstruct GA4 session data after a collection failure. The client-side session stitching, attribution modelling, and user-scoped dimensions are computed by Google’s pipeline. What you can do is reconstruct the business-critical events, especially purchases, and annotate your reports so historical comparisons stay honest.

Source 1: Server-Side GTM Request Logs

If you’re running server-side GTM (and for any serious ecommerce property, you should be), your sGTM container is a full log of every event that was supposed to go to GA4. Cloud Run and App Engine both retain request logs by default. During an incident, these logs are your ground truth.

Query Cloud Logging for POST requests to your GA4 tag endpoint during the outage window, extract the event payloads, and you have the raw hits. You can either replay them (risky — GA4 may deduplicate or reject old timestamps) or store them separately for reporting.

gcloud logging read \
  'resource.type="cloud_run_revision"
   AND resource.labels.service_name="sgtm-prod"
   AND httpRequest.requestUrl:"/g/collect"
   AND timestamp>="2024-09-01T14:00:00Z"
   AND timestamp<="2024-09-01T18:00:00Z"' \
  --format=json \
  --limit=100000 > outage_events.json

Parse the payloads, extract event names, client IDs, revenue values, and load them into a BigQuery table with the same schema shape as the GA4 export. Now you have queryable data for the outage window.

Source 2: Shopify Order Records

For ecommerce clients, revenue is the number that matters. Shopify’s order API is your source of truth here. Every completed order has a timestamp, a value, a customer ID, and (if you’re passing it via cart attributes or a custom app) the GA4 client_id and session_id.

We instrument every Shopify client to store the GA4 client_id and session_id on the cart as attributes, which then persist to the order as note attributes. This means we can rebuild purchase-level attribution even when GA4 collection fails entirely.

The reconciliation query:

WITH shopify_orders AS (
  SELECT
    order_id,
    created_at,
    total_price,
    JSON_EXTRACT_SCALAR(note_attributes, '$.ga_client_id') AS client_id,
    JSON_EXTRACT_SCALAR(note_attributes, '$.ga_session_id') AS session_id
  FROM `your-project.shopify.orders`
  WHERE created_at BETWEEN '2024-09-01 14:00:00' AND '2024-09-01 18:00:00'
),
ga4_purchases AS (
  SELECT
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS transaction_id,
    user_pseudo_id AS client_id,
    ecommerce.purchase_revenue AS revenue
  FROM `your-project.analytics_XXXXX.events_20240901`
  WHERE event_name = 'purchase'
)
SELECT
  s.order_id,
  s.total_price,
  s.client_id,
  CASE WHEN g.transaction_id IS NULL THEN 'MISSING_IN_GA4' ELSE 'PRESENT' END AS ga4_status
FROM shopify_orders s
LEFT JOIN ga4_purchases g ON s.order_id = g.transaction_id;

Everything marked MISSING_IN_GA4 needs to be accounted for in your backfill. You now know exactly which transactions were lost, their revenue, and which sessions they belonged to.

Source 3: The GA4 BigQuery Export Itself

Even during a collection failure, the export often contains partial data — some hits from some tag configurations made it through. Diff your Shopify order list against the export to find exactly what’s missing, then decide whether to reconstruct the missing rows in a shadow table or accept the gap and annotate your reporting.

Our preferred approach is a shadow table. We create a dataset called analytics_recovered with a purchase_events_recovered table that contains the reconstructed events with a source column indicating whether they came from GA4 export, sGTM logs, or Shopify. All downstream dashboards union GA4 data with the recovered table, so revenue reporting stays accurate. Session and user metrics we accept as lost.

This approach breaks when you don’t have client_id persisted on Shopify orders, or when the outage was long enough that sGTM logs have rotated out. Cloud Logging default retention is 30 days for _Default and 400 days for _Required. If you’re not exporting logs to BigQuery for long-term storage, you have a 30-day window to act. Set up a log sink to BigQuery. Do it before you need it.

Incident Response Checklist

When an incident hits, you need to work fast and communicate clearly. This is the checklist we run internally:

Within 15 minutes of alert:

  • Confirm the failure mode using the diagnostic sequence above
  • Screenshot the BigQuery evidence (intraday table row counts, missing tables)
  • Check Google’s status page and the GA4 announcements forum, note the timestamp
  • Post initial alert to client Slack: “We’ve detected a GA4 data collection anomaly starting at [time]. Investigating. Business systems (Shopify, ad platforms) are unaffected.”

Within 1 hour:

  • Pull sGTM request logs for the outage window and export to BigQuery
  • Run the Shopify reconciliation query and quantify missing revenue
  • Send stakeholder update with concrete numbers: “Between 14:00 and 17:30 UTC, GA4 did not receive approximately [X] purchase events representing [$Y] in revenue. We have full recovery data from Shopify and server-side logs.”

Within 24 hours:

  • Build or update the shadow recovery table
  • Annotate affected date ranges in Looker Studio or your reporting layer
  • Add a note in the GA4 UI itself via property annotations (if using the Data API, tag affected dates)
  • Send final incident summary with root cause (as far as known), duration, business impact, and recovery status

Within 1 week:

  • Retrospective: did the alert fire fast enough? Did the recovery data cover the gap? What’s missing from the stack?
  • Update the monitoring thresholds if you got false positives or missed the incident

The stakeholder communication piece is the one people underinvest in. During the September incident, teams that could tell clients within 30 minutes “yes, we’re aware, here’s the scope, here’s what we’re doing” retained trust. Teams that responded to client emails 6 hours later looked negligent, whether they were or not.

Common Mistakes and Troubleshooting

Treating the intraday table like the daily table. Some event parameters populate later. traffic_source fields, user properties set after the event, and some ecommerce fields may be null in intraday and populated in the final daily export. Don’t build revenue dashboards on intraday.

Alerting on total event volume only. A property can lose 100% of purchase events while page_view volume stays normal (a broken ecommerce tag, not a platform-wide outage). Segment your alerts by event category.

Replaying old hits to GA4 via Measurement Protocol. Tempting but dangerous. GA4 accepts events up to 72 hours old, silently drops older ones, and may deduplicate against transaction_id. You can end up with double-counted revenue or nothing at all. Prefer a shadow reporting table over replay.

Assuming BigQuery export is real-time. The intraday table has a few minutes of lag under normal conditions and much more during processing incidents. If your monitoring assumes sub-minute freshness, you’ll get constant false positives.

Not capturing evidence during the incident. Screenshots and log exports become critical when clients ask questions weeks later, or when you need to file a support case with Google. Capture aggressively; delete later if you don’t need it.

Building this only for GA4. The same pattern (baseline query, anomaly detection, shadow recovery table) applies to your Amazon SP-API data pipelines, Meta Ads exports, and any other analytics source where the vendor UI is the only visible interface. GA4 is the canonical example because it fails often enough that you’ll get to test your process.

Key Takeaways

  • The GA4 UI is not a monitoring tool. Build detection on the BigQuery export because it’s the only surface that gives you both raw data and query-level SLA.
  • A scheduled BigQuery query comparing hourly volume against a 7-day baseline, plus a Slack webhook, will catch most incidents within 15 minutes for under $5/month.
  • Diagnose the failure mode before you act. Collection failure, processing lag, and reporting lag each require a different response, and only collection failure needs a backfill.
  • Persist GA4 client_id and session_id on Shopify orders now, before you need them. Combined with sGTM request logs, this gives you full purchase reconstruction during any GA4 outage.
  • Build a shadow recovery table rather than replaying events to GA4. Replay is fragile, opaque, and risks double-counting.
#GA4#BigQuery#Data Quality#Incident Response

Share this article

Want This Implemented Correctly?

Let our team apply these concepts to your specific setup — with QA validation and 30 days of support.