BigQuery bills catch teams off guard because BigQuery is cheap in most scenarios, and then spectacularly expensive in a few. The difference between a 20 USD month and a 2,000 USD month is usually one bad scheduled query hitting an unfiltered GA4 events table every hour.
This guide covers the real cost structure, the seven optimizations that move the needle most, and the specific GA4 Export traps that generate the surprise invoices. Based on BigQuery cost audits and pipeline rescues I've done for European e-commerce, SaaS, and agencies since 2022.
If you're evaluating whether BigQuery makes sense at all, start with the BigQuery basics guide first. This article assumes you're already running BigQuery and want the bill under control.
Key Takeaways
On-demand pricing: 5 USD per TB scanned, 0.02 USD per GB/month storage, 0.01 USD/GB for data untouched 90+ days. First 1 TB query + 10 GB storage is free.
The biggest win is partition filtering with
_TABLE_SUFFIXon GA4 events tables. Adding one WHERE clause can cut a query from 500 GB scanned to 5 GB.
SELECT *on GA4 schema is the second biggest cost sink. Specify columns you actually use.Streaming inserts cost 10× more than batch inserts. If real-time isn't required, batch everything.
Query cache hits are free and kept 24 hours. Repeat queries from Looker Studio dashboards often hit cache; structure dashboards to exploit this.
Set a billing alert at 50% of your budget. Half of "unexpected" BigQuery bills could have been caught at 30% of the month if anyone was watching.
How BigQuery Pricing Actually Works
Three cost components. In order of typical impact on your bill:
1. Query Processing, 5 USD per TB scanned (on-demand)
Every time a query runs, BigQuery reports "bytes processed", the literal amount of data the query had to scan from disk. You pay 5 USD per 1,099,511,627,776 bytes (1 TB). Free tier covers the first 1 TB per month per billing account.
Critical detail: BigQuery is columnar. A query like SELECT user_pseudo_id FROM events on a 500 GB table only scans the user_pseudo_id column, not the whole 500 GB. That's typically 1-5% of total size. This is why specifying columns matters so much, SELECT * scans 100% of every column in every row returned.
2. Storage, 0.02 USD per GB/month (active), 0.01 USD (long-term)
Data unchanged for 90+ consecutive days automatically moves to long-term storage tier (50% cheaper). No action required. If a table gets an UPDATE or new row appended, it flips back to active. GA4 Export daily tables (events_YYYYMMDD) stop getting writes after their day is done, so they automatically hit long-term storage after 90 days.
For typical mid-market e-commerce with 3 years of GA4 Export: 30 GB active + 250 GB long-term = 30 × 0.02 + 250 × 0.01 = 3.10 USD/month. Storage is rarely the expensive line.
3. Streaming Inserts, 0.01 USD per 200 MB
Only if you use the streaming API for real-time writes. GA4 Export has both daily batch (free) and intraday streaming (charges apply). Most teams don't need intraday; if you don't, turn streaming off and save.
What's Not Included
- Free: storage uploads, metadata operations, queries that fail or are cancelled, queries served from cache, DDL statements (CREATE TABLE, etc.), DML on tables under 10 MB.
- Minimum charge per query: 10 MB. A query scanning 500 KB still charges for 10 MB. Tiny incremental queries add up if you run thousands per day.
The Seven Optimizations That Matter Most
Sorted by typical cost impact on a GA4 Export workload.
1. Partition Filtering with _TABLE_SUFFIX (Biggest Win)
GA4 exports data as one table per day: events_20251001, events_20251002, etc. The wildcard events_* lets you query across days. Without a suffix filter, the wildcard scans every day's table.
Bad (scans 365 days):
SELECT COUNT(DISTINCT user_pseudo_id)
FROM project.analytics_123.events_*
WHERE event_name = 'purchase'
Good (scans 7 days):
SELECT COUNT(DISTINCT user_pseudo_id)
FROM project.analytics_123.events_*
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
AND event_name = 'purchase'
For a 100,000-session-per-day ecommerce, that's 52× less data scanned, same answer. Just this one optimization typically cuts GA4 Export query costs by 70-90%.
2. SELECT Specific Columns, Never *
GA4 Export schema has nested columns (event_params, user_properties, items). SELECT * forces BigQuery to pull all of them, even if you only want event_name.
Bad: SELECT FROM events_, scans every column in every row.
Good: SELECT event_name, user_pseudo_id, event_timestamp FROM events_*, scans three columns only.
For GA4 tables, columns you pull from event_params via UNNEST also add to scan cost. Use only the parameters you actually need.
3. Cluster Tables on Frequently Filtered Columns
If you run queries regularly filtering by a specific column (e.g., country or event_name), cluster the table on that column. BigQuery physically sorts the data and can skip entire blocks during scan.
GA4 Export tables are already clustered on event_name, event_timestamp, and a few others, so event filters are already cheap. For your own derived tables, cluster them explicitly when you create them:
CREATE TABLE my_aggregates
PARTITION BY DATE(event_timestamp)
CLUSTER BY country, event_name AS
SELECT * FROM source
4. Preview Results Before Running Full Queries
BigQuery Studio shows "This query will process X bytes" before you hit Run. Check that number every time on an unfamiliar query. If it says 500 GB and you expected 5 GB, something's wrong, fix the query before executing.
For dashboards that run queries on behalf of users, log the bytes-processed metric and alert on outliers. A dashboard that normally processes 10 GB suddenly processing 500 GB means someone changed a filter or a data source upstream.
5. Exploit the 24-Hour Query Cache
Identical queries within 24 hours are served from cache. Cache hits are free and fast (sub-second).
In practice: Looker Studio dashboards that refresh every hour with identical parameters hit cache 23 out of 24 times. A dashboard with 10 queries running hourly for 10 hours of the workday would otherwise cost 10×10×query_size. Cache makes it 10×1×query_size. 10× savings without changing SQL.
Cache is per-query-text; any change in the query (even whitespace) invalidates. Keep query generation deterministic.
6. Materialize Repeated Aggregations
If five different dashboards all compute "daily revenue by channel", don't run the same aggregation five times, materialize it once to a small table and query that from dashboards.
Materialized views or scheduled queries running once daily off GA4 Export produce a 100 KB summary table. Five dashboards querying the 100 KB summary cost 5 × 10 MB minimum = 50 MB, versus 5 × 5 GB = 25 GB against the raw events table. Same answer, 500× cheaper.
7. Let Long-Term Storage Do Its Thing
Data untouched 90+ days automatically drops to 0.01 USD/GB/month (50% of active storage cost). Don't fight this by running UPDATE on historical partitions. Once a day is closed, leave it alone.
GA4 Export Specific Traps
These are the specific things that generate "my BigQuery bill tripled" support tickets.
Trap 1: Querying events_intraday_ When You Mean events_
GA4 creates two table families: events_YYYYMMDD (finalized daily batches) and events_intraday_YYYYMMDD (current day, streaming). Intraday is a separate set of tables. If your dashboard accidentally queries events_intraday_* with no date filter, you're scanning streaming data repeatedly.
Safer pattern: explicitly pick events_ (finalized) for historical reports, and only touch events_intraday_ if the report must include today.
Trap 2: UNNEST Without Reason
GA4 event parameters live inside a nested array. To filter by a parameter value, you UNNEST(event_params). That's fine. What's not fine is unnesting parameters you don't filter on:
Bad: SELECT event_name, (SELECT value.string_value FROM UNNEST(event_params) WHERE key='page_title') FROM events_*
This pulls the entire event_params array for every row. If most rows don't have a page_title parameter, you still paid to scan the whole array.
Better: Pre-filter by event_name = 'page_view' so only page view rows are unnested.
Trap 3: Running Scheduled Queries on Full History Instead of Yesterday
Scheduled Queries are great for building daily aggregates. The common mistake: writing the scheduled query as "rebuild full aggregation from start of time" instead of "append yesterday's data to existing aggregate".
Rebuild pattern (bad): CREATE OR REPLACE TABLE my_aggs AS SELECT ... FROM events_*. This runs daily. Scans all history daily. Costs multiply.
Append pattern (good): INSERT INTO my_aggs SELECT ... FROM events_* WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)). Runs daily, scans one day only.
Trap 4: Subqueries That Cross-Join Massive Tables
Joining GA4 Export to itself (e.g., finding users whose first event was X and then did Y) can blow up if done naively. Use session-window functions or narrower filters in each subquery before the join.
Mini-Story: 4,000 EUR to 400 EUR in Three Hours
When Alicja, head of analytics at a Polish D2C brand, showed me her Google Cloud bill in July 2025, BigQuery was eating 3,847 EUR/month. Her infrastructure was a standard GA4 Export + seven Looker Studio dashboards + three scheduled queries + one custom reporting pipeline.
Audit results:
- Three of the scheduled queries rebuilt full aggregates daily instead of appending yesterday's data. 80% of the bill came from these three queries.
- Two of the Looker dashboards were configured with 5-minute refresh intervals, cache-busting constantly. Once per hour was enough for her audience.
- One dashboard did
SELECTon theevents_wildcard with no suffix filter. Single query, 47 GB scanned, ran 40 times a day.
Fixes (3 hours of work):
- Rewrote scheduled queries as append-only incrementals
- Changed dashboard refresh intervals to hourly
- Added
_TABLE_SUFFIXfilter and specific column list to the bad dashboard
Next month's bill: 412 EUR. Same dashboards, same insights, 10× cheaper.
This is a typical result when a BigQuery cost audit finds low-hanging fruit. It's rarely an architecture problem. It's almost always specific queries someone wrote once and forgot about.
Think your BigQuery bill has similar fat to trim? Get in touch for a cost audit, I review your billing, your top 20 most expensive queries, and your scheduled jobs, then hand you a prioritized fix list. No long engagements, no retainer requirement.
Setting Up Budget Alerts and Query Limits
Two safeguards every BigQuery setup should have from day one.
1. Google Cloud Billing Budget + Alerts
In Google Cloud Console: Billing > Budgets & alerts > Create budget. Set amount (e.g., 100 EUR/month), and alert at 50%, 90%, and 100%. Emails go to your billing admins when thresholds hit.
This is your safety net. A runaway query at 3 AM will hit your email before it hits your CFO's desk.
2. Per-Query Bytes Billed Limit
BigQuery Studio: Settings > Custom quota > Per-query. Set a maximum bytes billed per query (e.g., 100 GB). Any query that would scan more than that limit fails immediately instead of running and billing.
For GA4 Export workloads, 100 GB is a reasonable ceiling, legitimate analytics queries usually scan under 50 GB. Anything above is a mistake.
3. Per-User Project Quota (Advanced)
For shared projects where multiple analysts query independently, set per-user quotas to prevent one analyst's bad query from eating everyone's budget. Settings > Quotas > Query usage per day, per user.
On-Demand vs Reserved Slots: When to Switch
Default BigQuery uses on-demand pricing (5 USD per TB). Once your monthly bill stabilizes in the 2,000-3,000 USD/month range, consider switching to capacity-based pricing with Editions (Standard/Enterprise/Enterprise Plus).
Editions pricing:
- Standard: 0.04 USD per slot-hour (used)
- Enterprise: 0.06 USD per slot-hour
- You commit to a baseline number of slots, pay for auto-scaling above that
Break-even: on-demand queries at 5 USD/TB translate to roughly 1 TB per 40 slot-hours under Standard Edition. If you're scanning 2 TB/day, reserved 100 slots at Standard Edition runs 24 × 0.04 × 100 = 96 USD/day, versus 2 × 5 = 10 USD/day on-demand, so on-demand wins for moderate volumes.
The break-even point typically sits around 30-50 TB/month of queries. Most mid-market teams never approach this.
Rule of thumb: Stay on-demand until your BigQuery bill exceeds 2,500 USD/month consistently. Below that, optimizing queries saves more money than switching pricing models.
Mini-Story: The Streaming Inserts Surprise
Kuba, data engineer at a European SaaS, inherited a BigQuery pipeline in February 2025 that ingested user events from a Kafka queue via streaming inserts. 50 million events per day. Monthly BigQuery bill: 890 EUR, of which 420 EUR was streaming inserts charges.
The business requirement wasn't actually real-time. Events were queried by dashboards updated hourly. Streaming was used because whoever built the pipeline two years earlier "needed real-time for a feature that got descoped."
Fix: swap streaming inserts for batch inserts every 15 minutes. Slightly more complex code (batching logic), but fully in-Python, no additional infrastructure.
Monthly streaming cost after switch: 0 EUR. Total BigQuery bill dropped to 470 EUR. The data freshness for dashboards was unchanged because they refreshed hourly anyway.
Streaming inserts are the right tool if you genuinely need sub-minute data freshness. If you don't, batch and save the 47% of the bill.
Monitoring Costs: The Three Queries to Run
You can audit your own BigQuery costs without third-party tools. Run these against INFORMATION_SCHEMA views.
Top 20 Most Expensive Queries (Last 30 Days)
SELECT
user_email,
query,
total_bytes_billed / POWER(10, 12) * 5 AS cost_usd,
total_bytes_billed / POWER(10, 9) AS gb_scanned,
total_slot_ms / 1000 AS slot_seconds,
creation_time
FROM region-eu.INFORMATION_SCHEMA. JOBS_BY_PROJECT
WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
AND statement_type!= 'SCRIPT'
ORDER BY total_bytes_billed DESC
LIMIT 20
This alone identifies the queries eating your budget. Usually 3-5 queries account for 80% of cost.
Query Cost by User
SELECT
user_email,
ROUND(SUM(total_bytes_billed) / POWER(10, 12) * 5, 2) AS cost_usd,
COUNT(*) AS queries
FROM region-eu.INFORMATION_SCHEMA. JOBS_BY_PROJECT
WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
GROUP BY user_email
ORDER BY cost_usd DESC
Useful for figuring out whether costs come from dashboards (service account user), scheduled queries (another service account), or human analysts.
Daily Cost Trend
SELECT
DATE(creation_time) AS day,
ROUND(SUM(total_bytes_billed) / POWER(10, 12) * 5, 2) AS cost_usd
FROM region-eu.INFORMATION_SCHEMA. JOBS_BY_PROJECT
WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 60 DAY)
AND job_type = 'QUERY'
GROUP BY day
ORDER BY day DESC
Run this weekly. Spikes are the signal, investigate any day that's more than 2× the 30-day median.
Want these queries set up in a Looker Studio dashboard that alerts on spikes? See my services for BigQuery monitoring implementations.
Frequently Asked Questions
Why is my BigQuery bill so high?
In 95% of cases the answer is one of five mistakes: scheduled queries rebuilding full aggregates daily, queries on GA4 events_ without a _TABLE_SUFFIX filter, SELECT on wide schemas, streaming inserts used when batch would work, or dashboards with too-frequent refresh intervals busting query cache. Run the INFORMATION_SCHEMA queries from this guide to identify which category your bill falls into.
How much does a typical GA4 Export cost on BigQuery?
For a mid-market European e-commerce with 100,000 sessions per day: 5-15 USD/month in storage, 10-40 USD/month in queries if dashboards are efficient, and 0 USD if you stick to batch exports. Total typical bill: 15-50 USD/month. Bills over 200 USD/month almost always have fixable inefficiencies.
Does BigQuery cache queries and is cache free?
Yes. BigQuery automatically caches query results for 24 hours. Cache hits don't count against your billing. Cache is per-query-text, so any change (including whitespace) invalidates. For dashboards with identical queries running multiple times a day, cache delivers 90%+ of requests for free.
When should I switch from on-demand to reserved slots?
When your monthly BigQuery bill exceeds 2,500 USD consistently. Below that threshold, optimizing queries with partition filtering and specific column selection saves more than switching pricing models. Reserved slots pay off when you have sustained high query volumes, typically 30+ TB/month.
How do I set a budget limit on BigQuery?
Two ways: Google Cloud Billing > Budgets & alerts for email notifications, and BigQuery Settings > Custom quota for a hard ceiling on bytes billed per query. Set the billing alert to 50% of expected monthly spend, and the per-query quota to 10× your typical largest legitimate query. These two together catch 99% of runaway cost events.
Does SELECT * really cost more than SELECT column_list?
Yes, significantly. BigQuery is columnar. SELECT scans every column for every row returned. On GA4 Export schema with nested event_params and user_properties, SELECT typically scans 10-50× more data than SELECT event_name, user_pseudo_id. The difference on a 100 GB table is scanning 100 GB (cost: 0.50 USD) vs 2-5 GB (cost: 0.01-0.03 USD).
Conclusion
BigQuery is cheap for teams who write efficient SQL and expensive for teams who don't. The pricing itself (5 USD per TB query, 0.02 USD per GB/month storage) is fair and free tier covers small workloads entirely.
If your bill surprises you, the cause is almost always in your queries, not in BigQuery's pricing. Run the INFORMATION_SCHEMA queries above, find your top 20 most expensive jobs, and apply the seven optimizations to those specific queries. Most teams recover 60-90% of their bill in an afternoon.
Set up billing alerts and per-query bytes-billed limits on day one. The cost of 15 minutes of setup is one month's worth of your eventual BigQuery bill, paid back the first time something goes wrong.
Want me to audit your BigQuery bill and identify where it's leaking? Get in touch for a scoped cost review. I analyze your top queries, scheduled jobs, and dashboards, and hand you a prioritized fix list with expected savings in EUR. No long engagements, just the optimization you can ship this week.
Want me to audit your BigQuery bill?
I analyze your top queries, scheduled jobs, and dashboards, then hand you a prioritized fix list with expected savings in EUR. No long engagements.
See my servicesNeed help? Get in touch
Have a question about your analytics setup? Fill out the form, I usually reply within 24 hours.