Skip to content

Pitfall #1: Sandbox Mode Silently Expires Your Data

By Timo Dechau · Last updated March 25, 2026

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.

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.”

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.

Run this against your GA4 dataset right now:

SELECT
table_name,
option_name,
option_value
FROM
`your_project.analytics_123456789`
.INFORMATION_SCHEMA.TABLE_OPTIONS
WHERE
option_name = 'expiration_timestamp'
AND table_name LIKE 'events_%'
ORDER BY table_name
LIMIT 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_value
FROM
`your_project.analytics_123456789`
.INFORMATION_SCHEMA.SCHEMATA_OPTIONS
WHERE
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.