70 credits a day for 96 minutes of work: why a warehouse bill climbs on flat data

  • Saurav Raj Joshi
  • 10 min read

A warehouse bill that climbs every month while no new data goes in isn’t a procurement problem. The unit price was never the issue. Nearly always it’s a handful of queries: a dashboard refreshing on a schedule nobody set, a full scan where a filter would do, a warehouse that never suspends. The order you look in decides whether that takes an afternoon or a month.

The money sits in warehouse minutes nobody uses. A Snowflake warehouse on a ten-minute auto-suspend bills eleven minutes for one minute of work. Twenty dashboard tiles refreshing every fifteen minutes turn that into 70 credits a day. Attribute per query first, then read the schedule.

What a Snowflake or Databricks bill is actually made of

A Snowflake bill carries three compute meters and a Databricks bill arrives as two invoices, and most teams read one of each. Snowflake bills credits for warehouse runtime. Separately it bills compute-seconds for serverless work that needs no warehouse: Snowpipe, automatic clustering, materialised view maintenance, serverless tasks. Third, cloud services. Snowflake’s page on understanding compute cost says usage for cloud services “is charged only if the daily consumption of cloud services exceeds 10%” of that day’s warehouse credits, so an orchestrator firing thousands of SHOW and metadata calls can lift a bill with no warehouse time behind it. (Serverless doesn’t count towards that 10% adjustment. Park that one; it comes back at dev clones.)

Databricks bills DBUs. On classic compute that’s one invoice of two: VMs, disks and egress come from the cloud provider. On jobs compute, where the DBU rate is lowest, the cloud half is usually the larger one; on all-purpose compute it usually isn’t.

Pull both invoices before you assume which one you’re optimising.

How to attribute Snowflake credits and Databricks DBUs to one query

On Snowflake, start with SNOWFLAKE.ACCOUNT_USAGE.QUERY_ATTRIBUTION_HISTORY: credits per query_id, execution only. Idle warehouse time is attributed to no query, so the attributed credits for a warehouse never add up to its WAREHOUSE_METERING_HISTORY total. But that difference is the idle bill, and the idle bill is the number you came for. The attribution view’s documentation also drops queries of roughly 100 milliseconds or less, and the view lags by up to eight hours.

Databricks hands you less. Join system.billing.usage to system.query.history and system.compute.warehouses on warehouse and time window, then be clear about what you have: an apportionment, not a per-query bill. The billing table records usage_metadata.warehouse_id and carries no statement_id, and the query history table carries no cost column at all. Serverless rolls DBUs and infrastructure into one figure, and even that figure lands on the warehouse rather than the statement.

Apportioning by hand means taking an hour of WAREHOUSE_METERING_HISTORY credits and spreading it over whatever QUERY_HISTORY says ran in that hour. Time-weighting over-attributes under concurrency. Two queries running side by side for ten minutes consume ten minutes between them, not twenty, and the duration you weight by includes queueing and compilation.

I’ve got this wrong in exactly that way. I apportioned by duration, named a dashboard as the top spender, and the real answer was a warehouse that never suspended between two cheap overnight jobs. Most of that hour was nothing happening at all.

QUERY_TAG is a session parameter that lands on the attribution view as well as QUERY_HISTORY, so you can join from the query side through to cost. Warehouse object tags are the ones that reach billing metadata, through ACCOUNT_USAGE.TAG_REFERENCES. Tag the warehouses your non-production accounts run on, before anybody clones one.

Does the result cache already pay for the dashboard refresh?

On Snowflake it should, and for most flat-data dashboards it does: an identical query against an unchanged table comes back from the result cache with no warehouse running at all. Snowflake’s page on querying persisted results says “the cache expires after 24 hours”, and each reuse resets that clock, to a ceiling of 31 days from the first run.

Kettlebridge is invented. It’s a 31-person company selling mooring and berth management software, a sector we don’t work in, and every number below was chosen so the arithmetic can be checked rather than measured from anybody’s account. Kettlebridge refreshes twenty dashboard tiles every fifteen minutes, around the clock, against tables written once a night. Twenty-four hours over fifteen minutes is 96 cycles a day; 96 cycles × 20 tiles is 1,920 queries a day, roughly 58,000 a month.

Nearly all of those should cost nothing. Check the cache hit rate in QUERY_HISTORY first. The cache-defeating patterns have names: relative windows resolved through CURRENT_TIMESTAMP(), non-deterministic functions (UUID_STRING, RANDOM, RANDSTR), BI tools that vary query text between runs, USE_CACHED_RESULT switched off, and a reuse rule that needs an exact text match down to case and aliases. Then find the last time a human actually looked at the thing.

What auto-suspend costs: 70 credits a day for 96 minutes of work

Say one minute of work per cycle. At a ten-minute auto-suspend each cycle bills eleven: one working, ten idle. Ninety-six cycles × 11 minutes is 1,056 minutes a day, which is 17.6 hours, and a Medium first-generation standard warehouse bills four credits an hour, so 17.6 × 4 is 70.4 credits a day for 96 minutes of work. A second route lands in the same place: the warehouse is awake 17.6 of 24 hours, a duty cycle of 73%, and a Medium running flat out for a day is 24 × 4 = 96 credits, of which 73% is 70.4. Drop auto-suspend to 60 seconds and each cycle bills two minutes, because Snowflake’s cost page says “credits are billed per-second, with a 60-second (i.e. 1-minute) minimum” and that “after 1 minute, all subsequent billing is per-second as long as the warehouse runs continuously”. So 96 × 2 is 192 minutes, 3.2 hours, 12.8 credits. One setting, 57.6 credits a day.

Now the part where the obvious estimate is wrong. Halve the dashboard — ten tiles rather than twenty — and the bill barely moves, because the warehouse still wakes 96 times and still pays ten idle minutes each time. Wake-ups drive the idle bill, not query count. Cut the schedule 4x instead, to hourly, and 24 × 11 is 264 minutes, 4.4 hours, 17.6 credits a day: better, and still beaten by the auto-suspend change. Databricks auto-termination has the same shape, priced per node type.

Queries that hold the warehouse awake

You pay for warehouse-seconds, not bytes, so the expensive query is the one that keeps the warehouse running longest. Bytes scanned is the symptom you search on, never the line on the invoice. SELECT * over a hundred-column table usually arrives as a view defined that way. A filter only saves money if the engine can prune with it:

sql

-- may not prune: the cast can hide created_at. Check partitions scanned.
WHERE CAST(created_at AS DATE) = '2026-10-01'

-- predicate on the column as stored; cast the literal, not the column
WHERE created_at >= '2026-10-01'::TIMESTAMP_NTZ
  AND created_at <  '2026-10-02'::TIMESTAMP_NTZ

Verify rather than assume, because both engines beat the worst case and neither beats it reliably: Snowflake’s page on micro-partitions says flatly that “not all predicate expressions can be used to prune”, and on Databricks generated columns reach a partition from a derived date for a short list of expressions only, with the docs telling you to run EXPLAIN and read the plan.

Rank your top twenty by attributed credits, not duration. Partitions scanned against partitions total sits in the Query Profile or in GET_QUERY_OPERATOR_STATS, which covers completed queries from the last 14 days and needs OPERATE or MONITOR on the warehouse. Reading half a table’s partitions to return one day of it is a decorative filter.

What accumulates while data volume stays flat

Plenty grows while row counts don’t. Kettlebridge’s dev account was cloned from production, so it inherited the warehouse size and the ten-minute idle setting along with the schema, and an untagged dev warehouse is invisible in exactly the report you’d use to find it. CI that starts its own compute on every push can outspend the thing it tests. File count accumulates too: a thirty-second trigger writing two files a minute per partition adds 2 × 1,440 × 30, about 86,000 files a month, so a full scan gets slower every week on flat volume. Compact towards files of 256MB to 1GB. Databricks autotunes file size to 256MB under 2.56TB and 1GB above 10TB; the 128MB figure still in circulation is the fixed target of legacy auto-compaction, which the same page calls “no dynamic sizing”.

Run the size-up test: capacity problem or query problem

A capacity problem has symptoms besides cost. Queries queue. Joins and sorts spill to local storage and then to remote. The curve moves with something you can point at: rows ingested, users added, a region switched on. A 3am load that matches nothing in the orchestrator is the other case entirely, and on flat data it’s the one I’d bet on.

There’s a direct test, and it works because the rate card doubles at each step. Credits per hour for first-generation standard warehouses, from Snowflake’s warehouse overview:

Warehouse size Credits/hour
X-Small 1
Small 2
Medium 4

Read that table narrowly. Those rates are first-generation standard warehouses (GENERATION = '1'); Gen2, Snowpark-optimised and newer types bill at other rates, written down only in the Service Consumption Table, and a credit’s price then depends on edition, region and contract. Run the query one size up with USE_CACHED_RESULT = FALSE, on a freshly resumed warehouse at each size, more than once, because the first run warms the local SSD cache. Half the time means compute-bound: same cost, sooner answer. Eighty per cent of the time is double the rate at 0.8 of the runtime, which is 1.6, so that query just got 60% dearer. A single-file scan, a serial UDF or a remote-read bottleneck won’t move whatever size you throw at it.

Does a bigger edition or a bigger warehouse fix the bill?

No, and three commercial moves get bundled together inside that question. An edition upgrade raises the price per credit and switches on features: multi-cluster warehouses are Enterprise and above, not Standard, and somebody still has to set the cluster counts afterwards. A committed-spend agreement lowers the price per credit and changes no performance characteristic whatsoever. Resizing, or adding clusters, buys throughput at a higher burn rate. Only the third clears a queue, and it earns its money only where queries genuinely queue.

None of the three touches the rate at which waste grows, because the waste is a schedule and a missing filter, and those scale with the dashboards and analysts you add. So settle which series you’re on. Plot attributed credits per unit of work — per dashboard refresh, per job run, per million rows ingested — against the count of units. Flat per unit with a rising count is capacity, and you should buy it. Rising per unit is in the queries, and no tier fixes that.

Most of the above is an afternoon with the account usage views and a change to two warehouse settings. If that’s where your bill is, you don’t need us, and I’d rather say so on the call than after the invoice.

Tell us what’s broken.

Twenty minutes, no deck. We’ll tell you straight whether this is work we should be doing, and if it isn’t, we usually know who should.

We come back within one business day.