Pitfall #1: Sandbox Mode Silently Expires Your Data
The Trap
Section titled “The Trap”You set up the GA4 BigQuery export using BigQuery’s free sandbox tier. Everything works. Data flows in daily. You feel good about it — you’re collecting raw event data without spending a cent. Then one day, months later, you go to run a year-over-year comparison and discover your oldest data is just… gone. No warning, no error message, no email from Google. It simply expired.
Why It Happens
Section titled “Why It Happens”BigQuery’s sandbox mode enforces a 60-day maximum table expiration. When your GA4 export creates daily tables like events_20250115, sandbox automatically stamps each one with a 60-day time-to-live. That’s the deal — free tier, limited retention.
Here’s where it gets nasty. When you eventually enable billing (and you will), BigQuery does not go back and remove those expiration settings from your existing tables. The dataset-level default expiration stays set. Every table that was created under sandbox rules keeps its original expiration timestamp. Your new tables might be fine, but your historical data is still on a countdown timer.
People discover this in one of two ways: they notice a gap in their historical data, or — worse — they don’t notice at all and just assume their dataset starts when they think it does.
The source of this gotcha is well-documented on ga4bigquery.com, and I’ve personally seen it catch multiple teams who assumed “enabling billing” meant “problem solved.”
The Fix
Section titled “The Fix”When you move from sandbox to a billing-enabled project, you need to explicitly clean up expiration settings at two levels:
1. Remove the dataset-level default expiration:
ALTER SCHEMA `your_project.analytics_123456789`SET OPTIONS ( default_table_expiration_days = NULL);2. Remove expiration from all existing event tables:
DECLARE tables ARRAY<STRING>;DECLARE i INT64 DEFAULT 0;
SET tables = ( SELECT ARRAY_AGG(table_name) FROM `your_project.analytics_123456789` .INFORMATION_SCHEMA.TABLES WHERE table_name LIKE 'events_%');
WHILE i < ARRAY_LENGTH(tables) DO EXECUTE IMMEDIATE FORMAT( 'ALTER TABLE `your_project.analytics_123456789.%s`' || ' SET OPTIONS (expiration_timestamp = NULL)', tables[OFFSET(i)] ); SET i = i + 1;END WHILE;Yes, you need to do both. The dataset-level change only affects future tables. Every existing table retains whatever expiration was set when it was created.
How to Check If You Have This Problem
Section titled “How to Check If You Have This Problem”Run this against your GA4 dataset right now:
SELECT table_name, option_name, option_valueFROM `your_project.analytics_123456789` .INFORMATION_SCHEMA.TABLE_OPTIONSWHERE option_name = 'expiration_timestamp' AND table_name LIKE 'events_%'ORDER BY table_nameLIMIT 20;If this returns rows, those tables have expiration dates set. If the timestamps are in the past, those tables are already gone. If they’re in the future, you still have time — but act now.
Also check the dataset-level default:
SELECT option_name, option_valueFROM `your_project.analytics_123456789` .INFORMATION_SCHEMA.SCHEMATA_OPTIONSWHERE option_name = 'default_table_expiration_days';If this returns a value, every new table created in the dataset will inherit that expiration. Remove it.
Look, this one stings because it’s entirely preventable. Five minutes of checking these settings after enabling billing can save you from losing months of irreplaceable historical data.