< blog
10 min read

How to Trace a Snowflake Cost Spike Back to the Query and the Person Who Ran It

Illustration of a glass dashboard with a cost chart and one sharp spike under a magnifying glass, next to a card showing a user, a query and a warehouse

TL;DR

  • A spike investigation has four questions: when did it start, which service moved, which queries sat inside that window, and who or what issued them.
  • Snowflake can answer all four from ACCOUNT_USAGE, but each view has a different delay (45 minutes to 8 hours) and a different blind spot. Know them before you trust a number.
  • Query-level credits leave out idle time. If the queries explain little of a warehouse’s hour, the problem is a warehouse left running, not a bad query.
  • The “person” is often a service account. The role, the query tag and the comment on the SQL usually name the pipeline behind it.
  • Snowflake also flags some spikes for you. Check Snowsight before you start digging.

Step 0: see whether Snowflake already flagged it

Snowflake detects cost anomalies at the account level without any setup. In Snowsight, go to Admin > Cost management > Anomalies. In SQL, ACCOUNT_USAGE.ANOMALIES_DAILY returns each day’s consumption in credits and whether that day was an anomaly.

Know what it will not do. Snowflake’s documentation says it needs at least 30 days of consumption before it can flag anything, does not flag changes when the last seven days used fewer than 10 credits, and sends notifications once a day. A spike that started this morning will not be in the list yet, and a spike on a small account may never be.

Step 1: find which service moved

Start wide. Credits come from more than warehouses: automatic clustering, serverless tasks, search optimization and AI services all bill under their own service type.

select usage_date, service_type, round(sum(credits_billed), 1) as credits_billed
from snowflake.account_usage.metering_daily_history
where usage_date >= dateadd(day, -14, current_date())
group by usage_date, service_type
order by usage_date desc, credits_billed desc;

Read down the days. If WAREHOUSE_METERING jumped, go to Step 2. If another service type jumped, the trail is in that feature’s own history view (for example AUTOMATIC_CLUSTERING_HISTORY for AUTO_CLUSTERING), not in query history. SERVICE_TYPE has more values than Snowflake’s documentation lists, so an unfamiliar name is normal. The view’s latency is up to three hours.

Step 2: find the hour and the warehouse

If warehouses moved, find the hour. WAREHOUSE_METERING_HISTORY is hourly.

select warehouse_name,
start_time,
round(credits_used_compute, 2) as compute_credits,
round(credits_attributed_compute_queries, 2) as query_credits
from snowflake.account_usage.warehouse_metering_history
where start_time >= dateadd(day, -3, current_timestamp())
order by credits_used_compute desc
limit 20;

Two columns, two stories:

What you see What it means Where to go
query_credits is close to compute_credits Queries did the work Step 3
compute_credits is much larger than query_credits The warehouse was running while little or nothing was executing: idle time, an auto-suspend that is too long, or something waking it constantly Look at the warehouse’s AUTO_SUSPEND setting and at what resumed it, not at expensive queries

CREDITS_ATTRIBUTED_COMPUTE_QUERIES does not include warehouse idle time, which is why the gap between the two columns is a useful number. It is NULL for Adaptive Warehouses, which have their own per-query view, QUERY_METERING_HISTORY.

Step 3: list the queries inside that hour

Now take the hour you found and rank queries by the credits Snowflake attributes to them. Replace the two timestamps.

select qa.query_id,
qa.user_name,
qh.role_name,
qa.warehouse_name,
qh.warehouse_size,
qa.query_tag,
round(qa.credits_attributed_compute, 3) as credits,
round(qh.total_elapsed_time / 1000) as seconds,
left(qh.query_text, 200) as query_start
from snowflake.account_usage.query_attribution_history qa
join snowflake.account_usage.query_history qh
on qh.query_id = qa.query_id
where qa.start_time >= '2026-10-12 14:00:00'::timestamp_ltz
and qa.start_time < '2026-10-12 15:00:00'::timestamp_ltz
order by qa.credits_attributed_compute desc
limit 20;

What this view does and does not include, per Snowflake’s documentation:

  • Credits are attributed per query, including the effect of resizing and multi-cluster scaling. Queries running at the same time share cost by weighted average of resource consumption.
  • Warehouse idle time is not included.
  • Queries of roughly 100 milliseconds or less are not included.
  • Costs for serverless features, data transfer, storage, cloud services and AI tokens are not included.
  • Latency is up to eight hours, the longest of the views here. If the spike was this morning, wait.

One query at the top with a large share of the hour is a runaway query. Many small queries with similar credits is a different problem, covered in Step 5.

Step 4: work out who, or what, ran it

USER_NAME is the first answer and often not the useful one. Pipelines usually run as a service account, so the question becomes which pipeline. Three fields help, in this order:

  1. Query tag. QUERY_TAG is the value set for the session. Teams that set it per job, model or dashboard can answer the question in one line. If yours do not, the Snowflake query tags guide shows how to set them for dbt, Python and BI tools.
  2. Role. ROLE_NAME in QUERY_HISTORY is the role active in the session, which often maps to a team.
  3. The SQL itself. Some tools write a comment into the statement. dbt, for example, appends a JSON comment with the model name to every query by default on Snowflake. Look at the end of QUERY_TEXT.

To see whether one user or tag accounts for the week, group the same view over a longer window:

select coalesce(nullif(query_tag, ''), '(untagged)') as query_tag,
user_name,
count(*) as queries,
round(sum(credits_attributed_compute), 2) as credits
from snowflake.account_usage.query_attribution_history
where start_time >= dateadd(day, -7, current_timestamp())
group by 1, 2
order by credits desc
limit 25;

A large (untagged) row for one user is the finding: you cannot tell what the work was for.

Step 5: classify the cause

Spikes tend to be one of five shapes. Match what you found.

Shape What you saw Typical cause
One runaway query One query dominates the hour in Step 3 A missing join condition or filter, a full scan, a query left to run on a large warehouse
A schedule that grew Many runs of the same query pattern A job moved from daily to every few minutes, or was duplicated
A warehouse left on Large gap between compute and query credits in Step 2 Auto-suspend too long, a connection that keeps the warehouse awake
A size or config change Same queries, larger WAREHOUSE_SIZE than before Someone raised the size “to be safe” and it stayed
Another service Step 1 shows a non-warehouse service type moving Clustering, a serverless task or an AI function

The second shape is hard to see query by query. Group by the parameterized hash, which treats queries that differ only in literal values as one pattern:

select query_parameterized_hash,
count(*) as runs,
round(sum(credits_attributed_compute), 2) as credits,
min(user_name) as sample_user,
min(query_tag) as sample_tag
from snowflake.account_usage.query_attribution_history
where start_time >= dateadd(day, -7, current_timestamp())
group by query_parameterized_hash
order by credits desc
limit 20;

For the third shape, check what the warehouse was set to and whether something is resuming it every few minutes. Snowflake bills per second with a 60-second minimum each time a warehouse starts, so a trickle of tiny queries that keeps resuming a large warehouse costs far more than the queries suggest.

Step 6: fix it, then make sure you hear about the next one

  • Runaway query: tell the owner, and consider a statement timeout on the warehouse or role that ran it.
  • Schedule that grew: change the schedule, or ask whether the data is read as often as it is refreshed.
  • Warehouse left on: lower AUTO_SUSPEND and find what keeps resuming it.
  • Size change: resize back and note why it was raised.
  • Then set a budget or alert so the next spike reaches a person the same day. Snowflake’s anomaly detection works at daily granularity, so for faster signals you need alerts on hourly usage.

How Seemore handles this

Seemore Data runs this investigation for you. Cost Anomaly Detection catches spikes and drops in spend and gives you root cause analysis that replaces days of investigation with a clear answer, and Real Time Monitoring shows every connected account live, down to the exact query behind a spike. You can also ask Seemore Assistant about your environment in plain language instead of writing the SQL above.

Frequently asked questions

How long after a spike can I see it in Snowflake?

It depends on the view. QUERY_HISTORY has a latency of up to 45 minutes. WAREHOUSE_METERING_HISTORY and METERING_DAILY_HISTORY are up to three hours, and the cloud services column in the warehouse view can take up to six. QUERY_ATTRIBUTION_HISTORY is up to eight hours.

Why don't the query credits add up to the warehouse credits?

Because query-level attribution leaves out warehouse idle time and queries of about 100 milliseconds or less. The difference between CREDITS_USED_COMPUTE and CREDITS_ATTRIBUTED_COMPUTE_QUERIES in the hourly view is the credits spent while no query was running.

How do I find the person if everyone shares one service account?

Use the query tag, the role and the SQL comment. If none of them identifies the job, that is the real finding: add tags per job so the next investigation takes minutes.

Does Snowflake alert me about spikes?

Account-level anomaly detection is always on, needs at least 30 days of history, and notifies once a day. Custom anomaly monitors scoped by tag and service type are documented as a preview feature. Neither replaces an hourly alert on a specific warehouse.

Can I do this on an Adaptive Warehouse?

QUERY_ATTRIBUTION_HISTORY does not include jobs on Adaptive Warehouses. Use QUERY_METERING_HISTORY for per-query credits there.