BigQuery Streaming Inserts Cost: Find What's Driving the Bill
Streaming ingestion is the BigQuery line item nobody budgets for. Run two INFORMATION_SCHEMA queries to find the tables behind it, then cut the cost.
To find what is driving your BigQuery streaming inserts cost, query region-us.INFORMATION_SCHEMA.STREAMING_TIMELINE_BY_PROJECT (swap in your own region) and group by table over the last 30 days. Multiply the bytes by the streaming rate, and apply the per-row minimum if you still use the legacy API. The tables at the top of that list are your fix targets. Most can move to free batch load jobs or the cheaper Storage Write API.
Streaming ingestion is the line on the BigQuery bill that nobody budgeted for. Query costs get dashboards and alerts. Storage gets a quarterly look. Streaming usually gets neither. It starts as one application writing events straight into a table, and it keeps running long after anyone remembers who set it up. A pattern we see repeatedly in growth-stage fintech and e-commerce teams is a raw events table fed by a service that retries aggressively. Nobody can say what that table costs per month, or whether the data needs to be queryable within seconds at all.
This post is the five-minute version: how to detect it, what to change, and how to stop it coming back.
What Does BigQuery Streaming Actually Cost?
It depends on which ingestion path your writers use, and many teams do not know which one they are on.
The Storage Write API costs $0.025 per GiB with the first 2 TiB a month free. The older streaming path costs $0.01 per 200 MiB, which works out at $0.05 per GiB, with each row counted as at least 1 KB. These prices are for the US as of October 2026. Ten million 200-byte events cost the same as 10 GB. Small rows are the trap, because you pay for padding you never sent.
At scale the gap is real. One 2026 analysis puts a pipeline ingesting 10 TB a month through legacy streaming at about $500 a month, against $200 on the Storage Write API once the free tier is applied. Meanwhile, batch loading is free and is the default choice unless the data has to be queryable within seconds of arriving.
Be realistic about the size of this problem. Streaming is rarely the biggest line on a BigQuery invoice. It is the one that grows silently, because it scales with event volume rather than with anything an analyst does. If you are also watching compute spend, our guide to Postgres to BigQuery replication covers when a continuous feed earns its keep and when batch is the better call.

📺 Watch: Overview of a Cost Optimization Session for Google BigQuery with DoiT
How Do You Find What's Driving Your BigQuery Streaming Inserts Cost?
Start with the legacy streaming view. It holds per-minute statistics for each table that receives streamed rows. Only the last 180 days of history are retained, so run this soon. You need bigquery.tables.list on the project, and you must include a region qualifier.
-- BigQuery: legacy streaming spend by table, last 30 days
-- Replace region-us with your dataset region (region-eu, region-europe-west2...)
SELECT
dataset_id,
table_id,
SUM(total_rows) AS rows_streamed,
ROUND(SUM(total_input_bytes) / POW(1024, 3), 2) AS gib_sent,
ROUND(SUM(total_input_bytes) / NULLIF(SUM(total_rows), 0)) AS avg_row_bytes,
-- floor estimate: applies the 1 KB per-row minimum per minute-bucket
ROUND(SUM(GREATEST(total_input_bytes, total_rows * 1024))
/ POW(1024, 2) / 200 * 0.01, 2) AS est_usd_30d
FROM `region-us`.INFORMATION_SCHEMA.STREAMING_TIMELINE_BY_PROJECT
WHERE start_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND error_code IS NULL
GROUP BY dataset_id, table_id
ORDER BY est_usd_30d DESC
LIMIT 20;
Read the output in this order:
- est_usd_30d: your ranked suspect list. Two or three tables usually account for nearly all of it.
- avg_row_bytes under 1,024: you are paying for padding. Every row is billed as 1 KB, so small events cost more than their size suggests.
- rows_streamed against how often anyone queries the table: if the consumer is a daily dbt run, you are paying for real-time freshness nobody uses.
The estimate is a floor. The 1 KB minimum applies per row, and this view only gives per-minute aggregates. Treat the number as directional and the ranking as reliable.
How Do You Separate Legacy Streaming From Storage Write API Spend?
The query above covers the legacy path. If your writers use the Storage Write API, which is the gRPC route and the default in most modern client libraries, that traffic shows up in a different view.
-- BigQuery: Storage Write API volume by table and stream type, last 30 days
SELECT
dataset_id,
table_id,
stream_type,
ROUND(SUM(total_input_bytes) / POW(1024, 4), 3) AS tib_30d
FROM `region-us`.INFORMATION_SCHEMA.WRITE_API_TIMELINE_BY_PROJECT
WHERE start_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND error_code IS NULL
GROUP BY dataset_id, table_id, stream_type
ORDER BY tib_30d DESC;
Sum the tib_30d column. If the total is under 2 TiB a month, that path is effectively free and you can stop worrying about it. If it is well over, each extra TiB costs about $25 at the rate above.
Then check for waste. Retry loops are the most common cause of inflated streaming volume we find. A downstream service times out, resends the whole batch, and you pay for the same rows more than once.
-- BigQuery: streaming errors by table, last 7 days (legacy path)
SELECT
table_id,
error_code,
SUM(total_requests) AS failed_requests,
SUM(total_rows) AS rows_affected
FROM `region-us`.INFORMATION_SCHEMA.STREAMING_TIMELINE_BY_PROJECT
WHERE start_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND error_code IS NOT NULL
GROUP BY table_id, error_code
ORDER BY failed_requests DESC;
A table that appears in both the cost list and the error list is your first call. Fix the writer before you touch anything else.
Running those queries for one project is fine. Checking every model in a dbt project is not. fintel-scan is a free, open-source (MIT) CLI that runs a set of cost and correctness checks across a whole dbt project locally, with no warehouse connection: uvx fintel-scan. It covers the project side of the bill, and the queries above cover the warehouse side.

How Do You Cut BigQuery Streaming Inserts Cost?
Pick the cheapest ingestion method that still meets the freshness the consumer needs. Not the freshness the engineer assumed they needed. We use a simple decision frame with clients:
- Consumer is a daily or hourly dbt run, or a morning dashboard: switch to batch load jobs. Land micro-batches as files in Cloud Storage every 5 to 15 minutes and load them. It is free, and nobody downstream notices.
- Consumer needs rows within seconds (fraud rules, live operations views): move from legacy streaming to the Storage Write API default stream. It is cheaper per GiB and has a free monthly allowance. In the right-sized cases below the free tier, the cost goes to zero.
- Consumer is a CDC feed from an operational database: check whether your replication tool streams row by row. Many can be configured to batch.
- Tiny rows, high volume: batch events in the writer before sending, so you are not paying the per-row minimum on 150-byte payloads.
There is a correctness reason to move as well. Legacy streamed rows sat in a buffer that could take up to 90 minutes to become queryable via DML. If a dbt incremental model uses MERGE or DELETE against a raw table that is still being streamed into, you can hit errors or inconsistent results. Batch-landed tables avoid that.
In one engagement with a payments company, an events table was fed by a service that streamed every webhook payload and retried on any non-200 response. Nothing downstream read it more often than hourly. Replacing the stream with five-minute batch loads removed the streaming charge and also stopped duplicate retried rows reaching the staging layer. The data team had assumed real-time was a requirement. It had been an implementation default.
How Do You Stop BigQuery Streaming Costs Creeping Back?
Treat the diagnostic as a monitor, not a one-off.
- Schedule the first query weekly and write the output to a small monitoring table. Alert when any table's est_usd_30d crosses a threshold you choose. Keep this in version control, not in someone's console history.
- Add source freshness checks in dbt on every raw streamed table. If you move a table to batch, the freshness check tells you immediately whether the change slowed it beyond what the consumer tolerates. If you want to be sure your sources are checked, see our guide to dbt source freshness config alongside documentation coverage.
- Record an owner and a freshness requirement for every table that receives streamed data. A one-line entry in the model or source description is enough. The cost problem is almost always an ownership problem.
- Make streaming a deliberate choice in design review. The default for a new ingestion path should be batch, with a written reason required for anything faster.
This is the same governance habit that keeps query costs in check: every spend line has a name beside it.
Frequently Asked Questions
Q: How much do BigQuery streaming inserts cost?
A: Legacy streaming inserts are priced at $0.01 per 200 MiB, with each row counted as at least 1 KB. The Storage Write API is $0.025 per GiB with the first 2 TiB each month free, based on US pricing as of October 2026. Batch loading from Cloud Storage is free.
Q: How do I check how much data I have streamed into BigQuery?
A: Query INFORMATION_SCHEMA.STREAMING_TIMELINE_BY_PROJECT for legacy streaming and WRITE_API_TIMELINE_BY_PROJECT for the Storage Write API. Both need a region qualifier such as region-us. Group by dataset_id and table_id and sum total_input_bytes to rank tables.
Q: Is the BigQuery Storage Write API cheaper than streaming inserts?
A: Yes. It costs roughly half the legacy rate per GiB before the free tier, and it avoids the 1 KB per-row minimum that penalises small events. The first 2 TiB each month is also free.
Q: When should I use batch loading instead of streaming into BigQuery?
A: Use batch loading whenever the consumer can tolerate data that is a few minutes to an hour old. That covers most dbt-driven reporting. Batch loads are free on the shared slot pool, so streaming is only worth paying for when rows must be queryable within seconds.
Q: Why is my BigQuery streaming inserts cost higher than my data volume suggests?
A: The usual causes are small rows billed at the 1 KB minimum, and retry loops that resend the same rows after a timeout. Check the average row size and the error-code query above. Both are visible in INFORMATION_SCHEMA within minutes.
If your BigQuery ingestion spend is climbing and nobody can say which table or writer is responsible, that is a fixable problem. At Fintel Analytics, we have helped fintech, payments and e-commerce teams audit their BigQuery and dbt estate end to end, finding the costly ingestion paths, rebuilding them as batch or right-sized streaming, and putting monitoring in place so the bill stays explainable. A short audit usually pays for itself with the first table you move off streaming.
