GA4’s standard BigQuery export delivers the previous day’s data by around 9:00 AM UTC. For most analytics use cases, day-old data is fine. But for launch monitoring, live campaign management, or detecting tracking fires breaking in production, you need data faster. GA4’s intraday BigQuery export streams events into a separate intraday table throughout the day, giving you near-real-time access to your event data with a typical lag of under 1 hour. This guide explains how intraday tables work, the gotchas in querying them, and how to build monitoring queries on top.

How Intraday Export Works

When you link GA4 to BigQuery and enable the daily export, GA4 automatically creates two table types. The events_YYYYMMDD table receives the complete, processed day’s data after the day closes. The events_intraday_YYYYMMDD table receives a continuous stream of today’s events as they are processed by GA4. The intraday table for today is created when the first events of the day arrive, and it is deleted after the final daily table is created and verified. At no point do you have both an events_20240115 and events_intraday_20240115 for the same completed day.

Intraday tables are not available for all GA4 properties by default. They require the BigQuery export to be enabled, and streaming export must be turned on separately in the GA4 BigQuery linking settings. Streaming export incurs additional BigQuery streaming insert costs (approximately $0.01 per 200MB), which are separate from the standard BigQuery storage and query costs.

Querying Intraday Tables Safely

The most important rule for intraday queries: never mix intraday and completed daily tables in the same query without understanding the consequences. The intraday table for today may have duplicate events because GA4’s streaming pipeline can deliver events multiple times before deduplication. The final daily table for completed days has already been deduplicated. If you union today’s intraday data with completed daily data, you may have duplicates in today’s portion.

-- Query intraday table for today's events
SELECT
  event_name,
  COUNT(*) AS event_count,
  TIMESTAMP_MICROS(MAX(event_timestamp)) AS latest_event_time
FROM `your_project.analytics_XXXXXX.events_intraday_*`
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE('UTC'))
GROUP BY event_name
ORDER BY event_count DESC

Building a Today-Plus-History Query

A common requirement is a dashboard showing the last 30 days including today. The query needs to combine completed daily tables (yesterday and earlier) with the intraday table (today). Handle this by querying them separately and unioning the results:

-- Combine historical + intraday data
WITH historical AS (
  SELECT
    PARSE_DATE('%Y%m%d', event_date) AS event_date,
    event_name,
    COUNT(*) AS events
  FROM `your_project.analytics_XXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN
    FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('UTC'), INTERVAL 30 DAY))
    AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('UTC'), INTERVAL 1 DAY))
  GROUP BY event_date, event_name
),
today AS (
  SELECT
    CURRENT_DATE('UTC') AS event_date,
    event_name,
    COUNT(*) AS events
  FROM `your_project.analytics_XXXXXX.events_intraday_*`
  WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE('UTC'))
  GROUP BY event_name
)
SELECT * FROM historical
UNION ALL
SELECT * FROM today
ORDER BY event_date DESC, events DESC
img

Intraday Monitoring for Tracking Breaks

One of the most valuable uses of intraday tables is detecting when tracking breaks in production — a deployment removes a GTM tag, a new page template does not load the dataLayer correctly, or a checkout redesign breaks the purchase event. By comparing today’s hourly event counts to the same hour from the previous 7 days, you can detect anomalies within hours instead of the next morning.

-- Detect event count anomalies vs. historical average
SELECT
  EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp) AT TIME ZONE 'UTC') AS hour_utc,
  event_name,
  COUNT(*) AS today_count,
  AVG(hist.avg_count) AS historical_avg,
  ROUND(COUNT(*) / NULLIF(AVG(hist.avg_count), 0) * 100 - 100, 1) AS pct_diff
FROM `your_project.analytics_XXXXXX.events_intraday_*`
CROSS JOIN (
  SELECT
    EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp) AT TIME ZONE 'UTC') AS hour_utc,
    event_name,
    AVG(event_count) AS avg_count
  FROM (
    SELECT EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp) AT TIME ZONE 'UTC') AS hour_utc,
    event_name, COUNT(*) AS event_count
    FROM `your_project.analytics_XXXXXX.events_*`
    WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
      AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
    GROUP BY hour_utc, event_name, event_date
  )
  GROUP BY hour_utc, event_name
) hist
WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE('UTC'))
  AND EXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp) AT TIME ZONE 'UTC') = hist.hour_utc
GROUP BY hour_utc, event_name, hist.avg_count
HAVING ABS(pct_diff) > 30
ORDER BY ABS(pct_diff) DESC

Schedule this query to run hourly using BigQuery scheduled queries, writing results to a monitoring table. Connect a Looker Studio alerting dashboard to this table, or use BigQuery’s integration with Google Cloud Monitoring to trigger alerts when anomaly rows appear. This transforms your GA4 BigQuery export from a passive reporting tool into an active tracking health monitor.

guide

Leave a Comment