BigQuery Cost Optimization: Slots, Pricing and Tips
A BigQuery bill has two parts, compute and storage, and nearly all the surprises are in compute. This guide explains both pricing models, prices three query shapes on a real table, gives the break-even between on-demand and slots, and lists the limits that stop one bad query becoming an expensive one.
GloryTecks Team
Training & Placement Faculty
Last updated: 2 October 2026
A BigQuery bill has two parts: compute, for running queries, and storage, for keeping data. Storage is predictable. Compute is where a single careless query can cost more than a month of careful ones. BigQuery cost optimization is therefore mostly three things: knowing what a query is charged for, choosing the pricing model that suits your workload, and setting limits before you need them.
Prices below are the list prices shown on Google's pricing page for the us-central1 region on 2 October 2026 (Google Cloud: BigQuery pricing). Rates vary by region and change over time, so check the page before budgeting.
The two ways to pay for queries
| On-demand | Capacity (editions) | |
|---|---|---|
| You pay for | Bytes the query reads | Slots used, over time |
| List price | USD 6.25 per TiB | USD 0.04 to 0.10 per slot-hour |
| Free allowance | First 1 TiB each month | None |
| Billing unit | Per query, 10 MB minimum | Per second, one-minute minimum |
| Cost of a bad query | Unbounded unless capped | Bounded by the reservation size |
A slot is a unit of compute, roughly a virtual CPU. Under capacity pricing you create a reservation in one of three editions and pay for the slots it uses:
- Standard, USD 0.04 per slot-hour: autoscaling only, up to 1,600 slots, no commitments.
- Enterprise, USD 0.06: adds baseline slots that are always available, and commitments.
- Enterprise Plus, USD 0.10: adds compliance and disaster recovery features.
In the two higher editions a one-year commitment is discounted by 20% and a three-year commitment by 40% (Google Cloud: BigQuery editions).
Some things are not charged for. Batch load jobs on the shared pool are free, and under on-demand pricing so are queries that fail and queries answered from the cache.
What a query really reads
On-demand billing counts logical bytes: the size of the values in the columns a query touches, before compression. The sizes are documented. An INT64, FLOAT64, DATE or TIMESTAMP is 8 bytes. A STRING is 2 bytes plus its UTF-8 length. A NULL is 0 (Google Cloud: data types).
That makes the cost of a query calculable. The script below does the calculation for a real table, the New York yellow taxi trips for January 2024 (NYC TLC: trip record data). It is a model built from the documented sizes and list prices, not a measured BigQuery bill.
# A cost model for BigQuery on-demand queries, built from the documented size of each data type and
# the list prices on Google's pricing page for us-central1 on 2 October 2026. It reads a real file to
# count values; it is not BigQuery, and it does not measure a BigQuery bill.
import duckdb
SOURCE = "../../dedata/yellow_tripdata_2024-01.parquet" # NYC yellow taxi trips, January 2024
PRICE_PER_TIB = 6.25 # on-demand, USD
SLOT_HOUR = {"Standard": 0.04, "Enterprise": 0.06, "Enterprise Plus": 0.10} # pay as you go, USD
TIB = 2 ** 40
con = duckdb.connect()
columns = con.execute(f"DESCRIBE SELECT * FROM read_parquet('{SOURCE}')").fetchall()
rows = con.execute(f"SELECT COUNT(*) FROM read_parquet('{SOURCE}')").fetchone()[0]
size = {} # logical bytes per column
for name, dtype, *_ in columns:
if dtype == "VARCHAR": # STRING: 2 bytes + UTF-8 length; NULL is 0
expr = f'SUM(2 + octet_length(encode("{name}")))'
else: # INT64, FLOAT64, TIMESTAMP: 8 bytes; NULL is 0
expr = f'8 * COUNT("{name}")'
size[name] = con.execute(f"SELECT {expr} FROM read_parquet('{SOURCE}')").fetchone()[0]
table_bytes = sum(size.values())
print(f"{rows:,} rows, {len(columns)} columns, {table_bytes / 2**20:,.0f} MiB of logical data\n")
three = size["tpep_pickup_datetime"] + size["PULocationID"] + size["total_amount"]
day_share = con.execute(f"""SELECT AVG(CASE WHEN CAST(tpep_pickup_datetime AS DATE) = DATE '2024-01-15'
THEN 1.0 ELSE 0 END) FROM read_parquet('{SOURCE}')""").fetchone()[0]
shapes = [("SELECT * over the month", table_bytes),
("three columns over the month", three),
("three columns, one day's partition", three * day_share)]
SCALE = 1_000 # the same table with a thousand times the rows
print(f"query shape bytes read at {SCALE:,}x cost per run run daily for 30 days")
for label, read in shapes:
scaled = read * SCALE
cost = scaled / TIB * PRICE_PER_TIB
print(f"{label:34} {read / 2**20:8,.1f} MiB {scaled / 2**30:8,.1f} GiB {cost:12,.2f} USD {cost * 30:14,.2f} USD")
print("\nslot-hours that one TiB of on-demand scanning would buy")
for edition, price in SLOT_HOUR.items():
print(f" {edition:16} {PRICE_PER_TIB / price:6.1f}")
print("\nworkload TiB read slot-hours on-demand Standard cheaper")
for label, tib, slot_hours in [("wide scans, light compute", 40, 1_500), ("narrow scans, heavy joins", 4, 2_500)]:
on_demand, capacity = tib * PRICE_PER_TIB, slot_hours * SLOT_HOUR["Standard"]
print(f"{label:28} {tib:8} {slot_hours:11,} {on_demand:10,.2f} {capacity:10,.2f} "
f"{'capacity' if capacity < on_demand else 'on-demand'}")2,964,624 rows, 19 columns, 411 MiB of logical data
query shape bytes read at 1,000x cost per run run daily for 30 days
SELECT * over the month 410.9 MiB 401.3 GiB 2.45 USD 73.48 USD
three columns over the month 67.9 MiB 66.3 GiB 0.40 USD 12.13 USD
three columns, one day's partition 1.8 MiB 1.7 GiB 0.01 USD 0.32 USD
slot-hours that one TiB of on-demand scanning would buy
Standard 156.2
Enterprise 104.2
Enterprise Plus 62.5
workload TiB read slot-hours on-demand Standard cheaper
wide scans, light compute 40 1,500 250.00 60.00 capacity
narrow scans, heavy joins 4 2,500 25.00 100.00 on-demandThe table is small, so the model also prices it at a thousand times the size, about three billion rows:
- Selecting every column reads all nineteen: 401 GiB at scale, USD 2.45 a run. Run by a dashboard every day, that is USD 73 a month for one query.
- Three columns read a sixth as much: USD 0.40 a run. Nothing changed but the column list.
- Three columns of one day's partition: USD 0.01 a run. The same question, more than 200 times cheaper than the first version.
No tuning skill was involved. Naming columns and filtering on the partition column are the two habits that matter most, which is why partitioning and clustering is a cost topic as much as a performance one. Note also what does not help: a LIMIT on a table that is not clustered leaves the bytes read unchanged.
On-demand or slots?
The second block of output gives the break-even. At list prices, the cost of scanning one TiB on demand buys 156 slot-hours in the Standard edition and 104 in Enterprise.
So the question to ask of a workload is how many slot-hours it uses per TiB it reads:
- Fewer than about 156: capacity pricing is cheaper. This is typical of wide scans with little computation.
- More: on-demand is cheaper. This is typical of heavy joins and sorts over modest data.
The two example workloads show both cases. Their inputs are illustrative, not measurements: 40 TiB read with 1,500 slot-hours costs USD 250 on demand and USD 60 in Standard, while 4 TiB with 2,500 slot-hours costs USD 25 on demand and USD 100 in Standard.
You do not have to guess your own ratio. BigQuery records both figures for every job, kept for 180 days (Google Cloud: JOBS view):
SELECT user_email,
COUNT(*) AS jobs,
ROUND(SUM(total_bytes_billed) / POW(2, 40), 2) AS tib_billed,
ROUND(SUM(total_slot_ms) / (1000 * 60 * 60), 1) AS slot_hours
FROM `region-us`.INFORMATION_SCHEMA.JOBS
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
GROUP BY user_email
ORDER BY tib_billed DESC;Divide slot-hours by TiB and compare the result with the break-even. The same query, grouped by user, is also the quickest way to find who and what is spending the money.
Cost is not the only consideration. Capacity pricing makes spending predictable, because the reservation size is a ceiling, and a query that would have been expensive is merely slow. On-demand needs no planning and suits small or irregular use. The two can be mixed, project by project.
Limits that prevent accidents
Before a query runs, the editor shows how many bytes it will process, and a dry run does the same from the command line. The estimate is an upper bound.
Per query, a maximum bytes billed setting makes BigQuery refuse a query whose estimate exceeds the limit, without charge (Google Cloud: estimate and control costs):
bq query --dry_run --use_legacy_sql=false \
'SELECT pickup_zone, COUNT(*) FROM analytics.trips GROUP BY pickup_zone'
bq query --maximum_bytes_billed=10737418240 --use_legacy_sql=false \
'SELECT pickup_zone, COUNT(*) FROM analytics.trips GROUP BY pickup_zone'The second command caps the query at 10 GiB.
Per day, custom quotas limit the data processed by a project or by each user. The project default is 200 TiB a day, which at list price is USD 1,250, and the per-user default is unlimited. Lower both. These quotas apply only to on-demand pricing (Google Cloud: custom query quotas).
Per reservation, under capacity pricing, the maximum number of slots is the cap.
Per table, require_partition_filter rejects queries that would scan every partition.
Storage, the quieter half
Storage is charged per GiB per hour, with the first 10 GiB each month free. Google's own example puts 1 TiB of active logical storage in us-central1 at USD 23.55 for a month. Three levers reduce it:
- Leave old data alone. A table or partition unmodified for 90 days moves to a lower long-term rate automatically. Each partition is judged separately, which is one more reason to partition by date. Loading, DML or overwriting resets the clock; querying does not.
- Expire what you do not need. Set a partition expiration on event tables and a default table expiration on scratch datasets.
- Choose the billing model per dataset. A dataset is billed on logical bytes or on physical, compressed bytes (Google Cloud: datasets). The physical rate is about 1.7 times the logical rate, so physical billing pays when your data compresses better than that. Under physical billing you also pay for time travel and fail-safe storage, so tables that are overwritten often cost more than they appear to.
Seven tips, in order of effect
- Select only the columns you need.
- Partition large tables and require the filter.
- Set a maximum bytes billed as a default for interactive work.
- Lower the daily quotas from their defaults.
- Materialize what dashboards query repeatedly, so each refresh reads a small table. See materialized views.
- Measure slot-hours per TiB before deciding between on-demand and an edition.
- Review the top ten jobs by bytes billed each month. A handful of queries usually accounts for most of the bill.
Query-level technique is covered in BigQuery SQL tips.
How is a BigQuery query charged?
What is a slot in BigQuery?
Is on-demand or capacity pricing cheaper?
How do I stop one query from running up a large bill?
Warehouse cost management is part of the Data Engineering course.
Sources
- BigQuery pricing (Google Cloud) — Google Cloud
- Data types (Google Cloud Documentation) — Google Cloud
- Understand BigQuery editions (Google Cloud Documentation) — Google Cloud
- Estimate and control costs (Google Cloud Documentation) — Google Cloud
- Create custom query quotas (Google Cloud Documentation) — Google Cloud
- JOBS view (Google Cloud Documentation) — Google Cloud
- Introduction to datasets (Google Cloud Documentation) — Google Cloud
- TLC Trip Record Data (NYC Taxi and Limousine Commission) — NYC Taxi and Limousine Commission
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master GCP?
Join GloryTecks’ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.
Related articles
BigQuery Explained: Google's Serverless Data Warehouse
What “serverless” means in BigQuery, why its bill depends on the bytes a query reads, and the three habits that keep that number small: select only needed columns, partition and cluster tables, and check a query’s cost before running it.
BigQuery SQL: Tips, Tricks and Optimization
BigQuery rewards a few specific habits. This guide gives twelve, each tied to Google's own guidance: read fewer columns, filter and aggregate before joining, do several measures in one pass with COUNTIF, guard division with SAFE_DIVIDE, and stop expecting CTEs to be cached. Three are shown on 2.9 million real trips.
Partitioning and Clustering in BigQuery
Partitioning splits a BigQuery table into segments that a query can skip; clustering sorts the storage blocks inside them. This guide explains both, measures the effect with a model on 2.9 million real trip rows, shows why clustering column order matters, and lists the filters that quietly stop pruning.