Why Is My Snowflake Bill So High? (And How to Actually Fix It)
There's a specific moment a lot of data teams know well. The monthly Snowflake invoice lands, the number is bigger than last month — again — and someone in finance forwards it with a short, pointed question: "What is driving this?"
And the honest answer, more often than not, is: nobody's completely sure. That's not a knock on anyone. Snowflake is built to make scaling effortless. Spin up a warehouse, point a few tools at it, let some pipelines run, and everything just works. The flip side is that the same things that make it easy to grow also make it easy to quietly overspend. The bill creeps up a little each month until it's a line item someone upstairs starts circling in red. The good news: a high Snowflake bill is almost never a mystery once you know where to look. In our experience the waste tends to hide in the same handful of places, and most of it is fixable without slowing your teams down or ripping anything out. Let's walk through where the money actually goes, and the exact queries you can run today to find yours.
First, a quick reframe: you're paying for time, not data People assume Snowflake bills are mostly about how much data they store. They're usually not. Storage is cheap and predictable. The big, swingy part of the bill is compute — and compute is billed by the second that a warehouse is switched on, whether or not it's doing anything useful at that moment. Read that last part again, because it's the whole game. A warehouse that's powered on but sitting idle still bills you. A query that takes four times longer than it should still bills you for all four. The bill is really a measure of how much warehouse time you're paying for, and a surprising slice of that time is waste.
Here's roughly how a typical bill breaks down:
Illustrative split — yours will differ — but the pattern holds: a big chunk of almost every bill is idle time and inefficient queries. That's money you can win back without giving anything up. Notice the two largest "leak" categories: idle warehouse time and inefficient queries. Those are the easiest wins, so that's where we'll spend most of our attention.
Leak #1: Warehouses that are on but doing nothing When you create a warehouse, Snowflake sets it to stay awake for 10 minutes after the last query before it suspends itself. Ten minutes sounds harmless. It isn't. Picture a warehouse that powers a dashboard or an analyst's ad-hoc work. Someone runs a 20-second query, then thinks for a few minutes, then runs another. With a 10-minute idle timer, that warehouse almost never gets the chance to switch off — it just hums along all day, billing you for hours of "thinking time" between short bursts of real work.
Same queries, same work, wildly different bill. The green is real work. The red is idle time you're paying for. Dropping the idle timer to 60 seconds turns most of that red into free, suspended time. This single setting is, in our experience, the most common reason a bill is higher than it needs to be. The fix is usually as simple as lowering AUTO_SUSPEND to 60 seconds for interactive warehouses — and for anything that doesn't truly need to be hot all the time. Find it yourself. This query shows you, per warehouse, how many credits you've burned, how much actual query work happened, and what share of the warehouse's running time was idle. Big credits with high idle % is your tell:
WITH wh_credits AS (
SELECT warehouse_name,
SUM(credits_used) AS credits
FROM snowflake.account_usage.warehouse_metering_history
WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY 1
),
wh_queries AS (
SELECT warehouse_name,
COUNT(*) AS query_count,
SUM(execution_time) / 1000 AS busy_seconds -- execution_time is in ms
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())
AND warehouse_name IS NOT NULL
GROUP BY 1
),
wh_uptime AS (
-- Billed uptime = time between each RESUME and the following SUSPEND
SELECT warehouse_name,
SUM(DATEDIFF('second', resumed_at, suspended_at)) AS uptime_seconds
FROM (
SELECT warehouse_name,
event_name,
timestamp AS resumed_at,
LEAD(timestamp) OVER (PARTITION BY warehouse_name ORDER BY timestamp) AS suspended_at
FROM snowflake.account_usage.warehouse_events_history
WHERE timestamp >= DATEADD('day', -30, CURRENT_DATE())
AND event_name IN ('RESUME_WAREHOUSE', 'SUSPEND_WAREHOUSE')
)
WHERE event_name = 'RESUME_WAREHOUSE'
GROUP BY 1
)
SELECT c.warehouse_name,
ROUND(c.credits, 1) AS total_credits,
q.query_count,
ROUND(q.busy_seconds / 60) AS exec_minutes,
ROUND(100 * (u.uptime_seconds - LEAST(q.busy_seconds, u.uptime_seconds))
/ NULLIF(u.uptime_seconds, 0)) AS idle_pct
FROM wh_credits c
LEFT JOIN wh_queries q USING (warehouse_name)
LEFT JOIN wh_uptime u USING (warehouse_name)
ORDER BY total_credits DESC;Idle % here is an approximation — it weighs billed warehouse uptime (from resume/suspend events) against query execution time. Because concurrent queries overlap, treat it as a floor on busy warehouses; multi-cluster warehouses need a closer look.
The result looks something like this:
Example output. The warehouses near the top with high credits and lots of idle time are exactly where to start. ADHOC_WH burning 188 credits for 31 minutes of real work? That's a 10-minute idle timer doing its thing.
Leak #2: Warehouses that are bigger than the job Snowflake warehouses come in t-shirt sizes, and each size up roughly doubles the cost per second. A Large costs about twice a Medium, a Medium about twice a Small, and so on. Teams tend to size up when something feels slow — and then never size back down. The result is a fleet of warehouses running a size or two larger than the work actually needs. The cruel part is you often can't feel the waste, because a too-big warehouse finishes fine; it just costs more to do so. The honest way to right-size is to look at whether queries are spilling (running out of memory and dragging slower) or sailing through with room to spare. If a warehouse never spills and clears its queue easily, it's a strong candidate to drop a size. This query surfaces warehouses that are spilling to disk — a sign they might genuinely need their size, so you don't shrink the wrong ones:
SELECT warehouse_name,
COUNT(*) AS queries,
SUM(bytes_spilled_to_local_storage) / POWER(1024,3) AS gb_spilled_local,
SUM(bytes_spilled_to_remote_storage) / POWER(1024,3) AS gb_spilled_remote
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD('day', -14, CURRENT_DATE())
AND warehouse_name IS NOT NULL
GROUP BY 1
ORDER BY gb_spilled_remote DESC;Warehouses near the bottom (little or no spilling) are usually safe to size down and test. Warehouses spilling heavily to remote storage are the opposite — they may need a bump, or they need their queries fixed (which brings us to the next leak).
Leak #3: A few expensive queries doing most of the damage Almost every account has a small number of queries quietly eating a big share of the compute. A nightly job that scans an entire table when it only needs yesterday's rows. A dashboard that re-runs a heavy aggregation every time someone blinks. A join that quietly explodes into billions of rows. Snowflake now makes these easy to catch. The QUERY_ATTRIBUTION_HISTORY view tells you the actual credit cost of individual queries. It doesn't carry the query text itself, so we join to QUERY_HISTORY to see what each expensive query was — then rank by cost and go straight to the worst offenders:
SELECT a.query_id,
LEFT(h.query_text, 90) AS query_preview,
a.warehouse_name,
ROUND(a.credits_attributed_compute, 3) AS credits
FROM snowflake.account_usage.query_attribution_history a
LEFT JOIN snowflake.account_usage.query_history h USING (query_id)
WHERE a.start_time >= DATEADD('day', -7, CURRENT_DATE())
ORDER BY credits DESC
LIMIT 25;(One useful detail: QUERY_ATTRIBUTION_HISTORY measures the cost of running the query and deliberately leaves out idle time — so it's a clean read on which queries are genuinely heavy, separate from the idle problem in Leak #1.) Once you can see the top 25, the fixes are usually familiar engineering: filter earlier so you scan less, cluster or partition the big tables your heavy queries hit, materialize a result that's recomputed constantly, and stop full-scanning tables that have a date column right there. Often, tuning the top ten queries moves the bill more than anything else on this list.
Leak #4: Serverless features running on autopilot Snowflake has a set of "set it and forget it" features — automatic clustering, materialized views, the search optimization service, and similar — that run on their own dedicated compute behind the scenes. They're genuinely useful. They also accrue credits quietly, sometimes on tables where they're not earning their keep. This query shows what your serverless and background features are costing by type, so nothing is running up a tab unnoticed (we exclude WAREHOUSE_METERING so you see the serverless lines, not your regular warehouse compute):
SELECT service_type,
ROUND(SUM(credits_used), 1) AS credits
FROM snowflake.account_usage.metering_daily_history
WHERE usage_date >= DATEADD('day', -30, CURRENT_DATE())
AND service_type <> 'WAREHOUSE_METERING'
GROUP BY 1
ORDER BY credits DESC;If automatic clustering shows up large, it's worth checking whether it's running on high-churn tables that actually benefit, or burning credits reorganizing data that nobody filters on. The same logic applies to materialized views and search optimization — keep them where they pay off, switch them off where they don't.
Leak #5: No guardrails, so nobody notices until the invoice The last one isn't a single setting — it's the absence of a safety net. Most surprised-by-the-bill accounts have no resource monitors (which can cap or alert on credit usage), no cost attribution (so you can't tell which team or pipeline is responsible), and no regular review. Spend grows in the dark and only surfaces once a month when it's already spent. Setting up a resource monitor takes minutes and gives you an early warning the next time something runs away:
CREATE OR REPLACE RESOURCE MONITOR monthly_guardrail
WITH CREDIT_QUOTA = 1000 -- set to your monthly budget in credits
FREQUENCY = MONTHLY
START_TIMESTAMP = IMMEDIATELY
TRIGGERS
ON 75 PERCENT DO NOTIFY
ON 90 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND;-- use SUSPEND_IMMEDIATE for a hard stop Pair that with tagging warehouses by team or workload, and suddenly "what is driving this?" has a one-line answer every month instead of a scramble. How much of this can you actually get back? It depends on how your account has grown, but the pattern is consistent: between tightening idle timers, right-sizing a few warehouses, and fixing the handful of queries doing the most damage, teams commonly take 30% to 60% off their compute bill — often within the first month, and usually without anyone noticing a difference in performance. (Those are industry-typical ranges, not a promise; your number depends on your setup.) The reason it works so well is that you're not cutting capability. You're cutting waste — the idle minutes, the oversized warehouses, the queries scanning ten times more than they need. The work still runs. It just stops costing you for the time it isn't running. Where to start this week If you only do three things, do these:
Run the first query above and find your top warehouses by credits, query time, and idle %. The idle offenders will jump out. Drop AUTO_SUSPEND to 60 seconds on your interactive and ad-hoc warehouses, and watch a week of bills. Pull your top 25 most expensive queries and tune the worst three or four.
That alone tends to move the number enough that the next "what is driving this?" email is a much more comfortable one to answer.
Want someone to find it for you? Going through every warehouse, query, and serverless feature takes time most data teams don't have to spare. That's exactly what we do at Insignyx — our Snowflake cost optimization work starts with a short, fixed-scope cost assessment: we look at where your credits are going and hand you a prioritized list of savings, with the numbers attached, before you commit to anything. Most of the time, the assessment pays for itself. If your bill has been climbing and you'd like a clear answer to where it's going, book a free Snowflake cost assessment — or explore our full Snowflake practice. The queries in this post run against the SNOWFLAKE.ACCOUNT_USAGE schema and need a role with access to it (such as ACCOUNTADMIN, or a role granted the relevant privileges). They're read-only — they look, they don't touch.
Follow Insignyx on LinkedIn
More on data, cloud & AI cost optimization.