Snowflake Cost Optimization: 7 Techniques That Actually Cut the Bill
Warehouse sizing, auto-suspend, clustering, ACCOUNT_USAGE queries, and resource monitors — estimate the bill from usage before you change prod.
By Dinesh Chandra
Table of contents
- 1. Aggressive auto-suspend
- 2. Find out where the money goes
- 3. Right-size warehouses (start small)
- 4. Separate workloads
- 5. Cluster only your biggest tables
- 6. Kill zombie schedules
- 7. Set resource monitors
- Query tags and a hard timeout
- Result cache is not a strategy
- COPY and Snowpipe are not “free compute”
- Time Travel and storage
- The habit that matters
- What I watch after a change
- FAQ
- What this means for data engineers
Snowflake bills compute by the second while a warehouse is running. Most savings come from running less, smaller, and smarter — not from a new product SKU.
These seven techniques are ordered by effort-to-impact. Estimate first from ACCOUNT_USAGE (queries below), then change one knob at a time so you can see what worked.
flowchart LR
query[Query] --> warehouse[Warehouse credits]
warehouse --> suspend[Auto-suspend]
warehouse --> size[Right-size]
query --> pruning[Partition pruning]
warehouse --> monitor[Resource monitors]
1. Aggressive auto-suspend
The default 10-minute auto-suspend burns money on idle time. For most ELT warehouses, 60 seconds is right:
alter warehouse transforming set
auto_suspend = 60
auto_resume = true;
The trade-off is losing the warehouse data cache on suspend. Keep a longer suspend (5–10 minutes) only on BI warehouses where the same dashboard SQL repeats all morning.
If a warehouse never suspends, someone left a session or a tool holding
it. QUERY_HISTORY will show you.
2. Find out where the money goes
Do not optimize a feeling. Look at 30-day credits by warehouse:
select
warehouse_name,
sum(credits_used) as credits,
round(credits * 3.0, 0) as approx_usd -- your contract rate
from snowflake.account_usage.warehouse_metering_history
where start_time > dateadd(day, -30, current_timestamp())
group by 1
order by 2 desc;
Most expensive queries (last 7 days):
select
query_id,
warehouse_name,
total_elapsed_time / 1000 as seconds,
partitions_scanned,
partitions_total,
left(query_text, 180) as query_text
from snowflake.account_usage.query_history
where start_time > dateadd(day, -7, current_timestamp())
and execution_status = 'SUCCESS'
order by total_elapsed_time desc
limit 20;
partitions_scanned close to partitions_total is a full scan —
usually a missing filter, a function on the filter column, or a table
that needs clustering.
Multiply credits by your contract rate if you think in dollars.
ACCOUNT_USAGE lags. For “what is on fire right now” use
INFORMATION_SCHEMA query history in the account.
Idle vs scan is a different diagnosis. A warehouse that never suspends is an idle problem. A query that scans every micropartition is a SQL problem. Do not buy a larger warehouse for the first one.
flowchart TD
bill[Credit spike] --> idle{Warehouse rarely suspends?}
idle -->|yes| suspend[Auto-suspend + kill held sessions]
idle -->|no| scan{partitions_scanned ≈ partitions_total?}
scan -->|yes| sql[Fix filter / clustering / SELECT *]
scan -->|no| size[Right-size or split BI vs ELT]
Idle first, then scans, then size. Size last.
Who is holding the warehouse:
select
warehouse_name,
user_name,
count(*) as queries,
sum(total_elapsed_time) / 1000 as seconds
from snowflake.account_usage.query_history
where start_time > dateadd('day', -1, current_timestamp())
and execution_status = 'SUCCESS'
group by 1, 2
order by seconds desc
limit 20;
3. Right-size warehouses (start small)
Each size doubles credits per hour. A Large that finishes in 10 minutes costs the same as a Medium that finishes in 20 — if the job scales.
If Medium finishes in 12 minutes, you just saved ~40%.
Test one size down on a representative run. Use the mental model that size doubles credits per hour: linear vs “does not scale” cases. Tiny SQL and single-file copies do not scale; big scans and transforms often do.
alter warehouse transforming set warehouse_size = 'SMALL';
Multi-cluster is for concurrency, not a substitute for a bigger size. If one query is slow, add size or fix the SQL. If 40 dashboards queue, add clusters.
4. Separate workloads
One warehouse per workload type:
| Warehouse | Work |
|---|---|
loading |
COPY, Snowpipe-adjacent bursts |
transforming |
dbt / Tasks |
bi |
Tableau / Looker / Hex |
A Monday backfill should not force a Large on the CFO’s dashboard. Split warehouses also make the metering view readable.
5. Cluster only your biggest tables
Clustering keys help pruning on multi-terabyte tables that are always filtered the same way (usually a date).
alter table analytics.gold.events
cluster by (event_date);
Do not cluster a 20 GB dimension. Automatic clustering is a background
credit spend. If automatic_clustering_history costs more than the
queries you saved, drop the key.
select
table_name,
sum(credits_used) as clustering_credits
from snowflake.account_usage.automatic_clustering_history
where start_time > dateadd(day, -30, current_timestamp())
group by 1
order by 2 desc;
Search Optimization Service is the other paid accelerator — point lookups (SKU, email), not analytics scans. Enable it for that use case only.
6. Kill zombie schedules
Hourly dashboards nobody opened this quarter are a warehouse that never
sleeps. Cross-reference query_history with BI usage. Disable the
schedule or raise the grain to daily.
The same applies to Tasks that still run after the product died.
7. Set resource monitors
The circuit breaker:
create resource monitor monthly_cap
with credit_quota = 500
frequency = monthly
start_timestamp = immediately
triggers
on 75 percent do notify
on 100 percent do suspend;
Attach it to the warehouses that can surprise you (dev, ad-hoc, a new vendor tool). Notify at 75% so you have time to act. Suspend at 100% on non-prod first; prod suspends need a page-ready owner.
Monitors do not cap Cortex tokens or storage. Those are separate bills. See the Cortex guide.
Query tags and a hard timeout
A warehouse shared by dbt and a BI tool is a mystery until every job sets a tag. Then Monday’s credit spike has a name.
alter session set query_tag = 'dbt:fct_orders:prod';
alter warehouse transforming set statement_timeout_in_seconds = 1800;
Thirty minutes is plenty for a well-partitioned incremental. A runaway Cartesian product should die, not finish the month’s credits. Put the timeout on the warehouse, not only in a worksheet you close.
flowchart LR
dbt[dbt models] --> t[transforming]
bi[Dashboards] --> b[bi]
copy[COPY / Snowpipe] --> l[loading]
t --> meter[ACCOUNT_USAGE]
b --> meter
l --> meter
One warehouse per job type. Metering is readable only after you split.
Result cache is not a strategy
Snowflake can return a cached result when the SQL text and the underlying data have not changed. That is a gift for a repeated worksheet. It is not a warehouse-sizing plan.
Do not “optimize” a dashboard by hoping the cache hits. If the underlying table ticks every five minutes, the cache will miss and you will pay the warehouse again. Materialize the grain. Then the dashboard is cheap for a different reason.
RESULT_SCAN(last_query_id()) is for chaining worksheets, not for
production jobs.
COPY and Snowpipe are not “free compute”
COPY INTO still wakes a warehouse. Snowpipe has its own credit
meter. A landing job that lists a million tiny files will spend
more on listing than on bytes.
select
pipe_name,
sum(credits_used) as pipe_credits
from snowflake.account_usage.pipe_usage_history
where start_time > dateadd(day, -30, current_timestamp())
group by 1
order by 2 desc;
If the pipe is expensive, fix file size (aim for a few hundred MB compressed) before you buy a larger loading warehouse.
Time Travel and storage
Compute is the noisy line item. Storage from long Time Travel on bronze is the quiet one. Set retention in the table PR. Details in Time Travel.
The habit that matters
Fifteen minutes every Monday: top warehouses, top queries, any warehouse
that did not suspend. Put the two ACCOUNT_USAGE queries in a saved
worksheet. Cost work that is only a Q4 project always returns.
What I watch after a change
- Credits per warehouse vs last week (same weekday)
- Share of queries with
partitions_scanned = partitions_total - Warehouses with zero auto-suspend events
- Time Travel bytes on bronze (see Time Travel)
If credits dropped and dashboards got slower, you sized down a job that does not scale. Put it back one size and fix the SQL.
FAQ
Does a bigger warehouse always cost more? Per hour, yes — each size doubles. Per query, no: a Large that finishes in half the time of a Medium can break even. Test a representative run.
Will auto-suspend at 60 seconds hurt BI? It can, because you lose the warehouse cache. Keep 5–10 minutes on the BI warehouse; use 60 seconds on ELT.
Do resource monitors cap Cortex and storage? No. They cap warehouse credits on the objects you attach. Token and storage bills are separate.
Should I cluster every large table?
No. Cluster tables that are filtered the same way and large enough
that pruning pays for automatic clustering. Check
automatic_clustering_history.
Is SELECT * a cost bug? On columnar storage, yes — you pay to scan columns nobody needs. Name the columns on hot queries.
What this means for data engineers
Cost work is a weekly habit: metering, top queries, then one knob. Do not wait for finance to forward the invoice.
Enjoyed this post?
Get the next one in your inbox — one email a week, no spam.
Newsletter signup is not live yet. Use the contact form if you want to be notified.