# Snowflake Monitoring: Queries, Warehouses and Credit Costs

> Snowflake monitoring guide: ACCOUNT_USAGE views and their delay, the query and warehouse metrics that matter, idle credits, resource monitors and alerts.

Source: https://last9.io/blog/snowflake-monitoring/

Snowflake monitoring means watching three things: how queries perform, how busy each virtual warehouse is, and how many credits those warehouses burn. Snowflake records all three in its own SNOWFLAKE database. The ACCOUNT_USAGE views keep a year of history but run up to 3 hours behind, so they suit trends and cost reports better than live alerting. The signals that matter most are queue time, bytes spilled to storage, partition pruning and idle credits.

This guide covers where Snowflake keeps its monitoring data, which columns to watch, SQL you can run today, what resource monitors can and can't do, and which alerts to set first. Column names and definitions come from Snowflake's [Account Usage documentation](https://docs.snowflake.com/en/sql-reference/account-usage) and its warehouse and cost guides.

## What should you monitor in Snowflake?

Snowflake runs the infrastructure, so you watch how your workload uses it. Four areas cover most problems:

- **Queries:** elapsed time, compilation time, bytes scanned, partitions scanned against the total, bytes spilled, cache use and failures.
- **Warehouses:** queries running and queued, time spent queued, and how often warehouses suspend and resume.
- **Credits:** credits per warehouse per hour, idle credits, and cloud services credits.
- **Guardrails:** resource monitor quotas and budgets, and which warehouses have neither.

Query performance and cost are linked here. A query that spills or scans every partition runs longer, and a longer query keeps the warehouse running, which uses credits.

## Where does Snowflake keep monitoring data?

Snowflake gives you two sources, and they trade freshness for history.

**ACCOUNT_USAGE** is a schema in the shared SNOWFLAKE database. Its views keep 1 year (365 days) of history and include dropped objects. According to Snowflake's documentation, most views have about 2 hours of latency, and the range runs from 45 minutes to 3 hours.

**INFORMATION_SCHEMA** exists in every database. It has no latency, so it shows what is happening now, but it keeps between 7 days and 6 months of history depending on the view or table function, and it excludes dropped objects.

The latency decides what each source is good for. An alert built on WAREHOUSE_METERING_HISTORY can fire hours after a runaway warehouse started. For live checks, use INFORMATION_SCHEMA table functions such as `QUERY_HISTORY()`. For trends, cost reports and anything older than a week, use ACCOUNT_USAGE.

By default only ACCOUNTADMIN can read ACCOUNT_USAGE. For a monitoring user, create a dedicated role and grant it access, either through SNOWFLAKE database roles (USAGE_VIEWER covers the warehouse views, GOVERNANCE_VIEWER covers QUERY_HISTORY) or with imported privileges:

```sql
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE monitoring_role;
```

Queries against these views run on a warehouse like any other query, so give monitoring its own X-Small warehouse and its own budget.

## Which Snowflake query metrics matter?

QUERY_HISTORY has one row per query. These are the columns worth tracking:

| Column                                   | What it tells you                                                                |
| ---------------------------------------- | -------------------------------------------------------------------------------- |
| `TOTAL_ELAPSED_TIME`                     | Total query time in milliseconds                                                 |
| `COMPILATION_TIME`, `EXECUTION_TIME`     | Where the time went, in milliseconds                                             |
| `QUEUED_OVERLOAD_TIME`                   | Milliseconds queued because the warehouse was overloaded by its current workload |
| `QUEUED_PROVISIONING_TIME`               | Milliseconds queued while the warehouse was created, resumed or resized          |
| `BYTES_SPILLED_TO_LOCAL_STORAGE`         | Data that didn't fit in memory and went to local disk                            |
| `BYTES_SPILLED_TO_REMOTE_STORAGE`        | Data that didn't fit on local disk either and went to remote storage             |
| `PARTITIONS_SCANNED`, `PARTITIONS_TOTAL` | How well the query pruned micro-partitions                                       |
| `PERCENTAGE_SCANNED_FROM_CACHE`          | Share of data read from the warehouse cache, from 0.0 to 1.0                     |
| `EXECUTION_STATUS`                       | success, fail or incident                                                        |
| `QUERY_PARAMETERIZED_HASH`               | Groups runs of the same query with different literal values                      |
| `QUERY_TAG`                              | The tag your application or job set for the session                              |

Start with patterns. A single slow query matters less than a moderately slow query that runs ten thousand times a day, and `QUERY_PARAMETERIZED_HASH` groups those runs together:

```sql
SELECT
  query_parameterized_hash,
  ANY_VALUE(query_text) AS sample_query,
  COUNT(*) AS runs,
  SUM(total_elapsed_time) / 1000 AS total_seconds,
  AVG(total_elapsed_time) / 1000 AS avg_seconds
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE start_time >= DATEADD('day', -7, CURRENT_TIMESTAMP())
GROUP BY query_parameterized_hash
ORDER BY total_seconds DESC
LIMIT 20;
```

Then look for the two most common causes of slow Snowflake queries. Spilling to remote storage means the warehouse ran out of memory and local disk for that query. A scan of every partition in a large table means the filter didn't prune anything:

```sql
SELECT
  query_id,
  warehouse_name,
  warehouse_size,
  bytes_spilled_to_local_storage,
  bytes_spilled_to_remote_storage,
  partitions_scanned,
  partitions_total,
  total_elapsed_time / 1000 AS seconds
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE start_time >= DATEADD('day', -1, CURRENT_TIMESTAMP())
  AND (bytes_spilled_to_remote_storage > 0
       OR (partitions_total > 1000 AND partitions_scanned = partitions_total))
ORDER BY bytes_spilled_to_remote_storage DESC
LIMIT 50;
```

Spilling usually points to a warehouse too small for that query, or a query that needs rewriting. Poor pruning points to filters that don't match how the table is clustered. Setting `QUERY_TAG` from your jobs and services makes all of this much easier, because you can group by team, pipeline or endpoint.

## How do you tell if a Snowflake warehouse is too small or too busy?

The two queue columns answer different questions. Provisioning time is the wait while a suspended warehouse resumes or a resize takes effect. Overload time is the wait because the warehouse is already running as many queries as it can.

```sql
SELECT
  warehouse_name,
  COUNT(*) AS queries,
  SUM(IFF(queued_overload_time > 0, 1, 0)) AS queued_for_overload,
  AVG(queued_overload_time) / 1000 AS avg_overload_seconds,
  AVG(queued_provisioning_time) / 1000 AS avg_provisioning_seconds
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE start_time >= DATEADD('day', -1, CURRENT_TIMESTAMP())
  AND warehouse_name IS NOT NULL
GROUP BY warehouse_name
ORDER BY queued_for_overload DESC;
```

WAREHOUSE_LOAD_HISTORY shows the same thing over time. Snowflake defines load as the total execution time of all queries in a given state during an interval, divided by the length of that interval. An `AVG_QUEUED_LOAD` above zero means queries were waiting, and `AVG_RUNNING` shows how much work was running at the time:

```sql
SELECT
  warehouse_name,
  DATE_TRUNC('hour', start_time) AS hour,
  AVG(avg_running) AS avg_running,
  AVG(avg_queued_load) AS avg_queued_load
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_LOAD_HISTORY
WHERE start_time >= DATEADD('day', -7, CURRENT_TIMESTAMP())
GROUP BY 1, 2
ORDER BY 1, 2;
```

The fix depends on which queue you see. Snowflake's warehouse guidance says resizing helps individual queries run faster but "is not intended for handling concurrency issues", while multi-cluster warehouses are "designed specifically for handling queuing". So steady overload queueing calls for more clusters, spilling calls for a larger size, and provisioning time is the price of aggressive auto-suspend.

## How do you monitor Snowflake credit usage?

Each warehouse size uses twice the credits per hour of the size below it. For standard Gen1 warehouses, Snowflake lists an X-Small at 1 credit per hour, a Medium at 4 and a 4X-Large at 128. Billing is per second, "with a 60-second minimum each time the warehouse starts".

That doubling is why one resize can change a monthly bill more than any query change. Track credits per warehouse per day from WAREHOUSE_METERING_HISTORY, which reports hourly:

```sql
SELECT
  warehouse_name,
  DATE_TRUNC('day', start_time) AS day,
  SUM(credits_used) AS credits
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY 1, 2
ORDER BY 2, 3 DESC;
```

Idle credits are the easiest savings to find. `CREDITS_ATTRIBUTED_COMPUTE_QUERIES` covers only compute used by queries and, in Snowflake's words, "doesn't include warehouse idle time usage". Subtracting it from `CREDITS_USED_COMPUTE` gives the credits a warehouse burned while it was running with nothing to do. This query comes from Snowflake's documentation:

```sql
SELECT
  (SUM(credits_used_compute) -
    SUM(credits_attributed_compute_queries)) AS idle_cost,
  warehouse_name
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
WHERE start_time >= DATEADD('days', -10, CURRENT_DATE())
  AND end_time < CURRENT_DATE()
GROUP BY warehouse_name;
```

High idle cost usually means auto-suspend is set too long. Snowflake suggests auto-suspend at around "5 or 10 minutes or less" for most workloads. The trade-off is that the warehouse cache "is dropped when the warehouse is suspended", so a warehouse serving repeated dashboard queries may be faster, and sometimes cheaper, with a longer setting. Watch `PERCENTAGE_SCANNED_FROM_CACHE` and provisioning time before and after you change it.

For more on tying spend to the work that causes it, see our guide to [cloud cost management for observability](https://last9.io/blog/cloud-cost-management-for-observability/).

## What do Snowflake resource monitors do?

A resource monitor sets a credit quota for one or more warehouses and acts when usage crosses a percentage of it. Only ACCOUNTADMIN can create one. This example from Snowflake's documentation suspends a warehouse at 1,000 credits:

```sql
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE RESOURCE MONITOR limit1 WITH CREDIT_QUOTA=1000
  TRIGGERS ON 100 PERCENT DO SUSPEND;
ALTER WAREHOUSE wh1 SET RESOURCE_MONITOR = limit1;
```

Each monitor can have one Suspend action, one Suspend Immediate action and up to five Notify actions. Suspend lets running statements finish, while Suspend Immediate cancels them. Quotas reset at 12:00 AM UTC, whatever start time the monitor has.

Treat resource monitors as a backstop with three limits:

- They "work for warehouses only". Serverless features need a budget instead.
- Snowflake says they "are not intended for setting precise limits on credit usage". A warehouse can keep using credits after it reaches the quota.
- They act on credits alone. A warehouse that queues every query all day can stay well inside its quota.

A sensible pattern is a Notify trigger at 75 to 90 percent for your on-call channel, and a Suspend trigger at 100 percent for warehouses where stopping work is acceptable.

## Which alerts should you set for Snowflake?

Start with cost and failures, then add performance. Run these checks on a schedule against ACCOUNT_USAGE, and keep in mind the latency of each view. The thresholds are starting points to tune:

| Alert              | Source                           | Condition                                                                |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------ |
| Credit spike       | `WAREHOUSE_METERING_HISTORY`     | Daily credits for a warehouse above 1.5x its 14-day average              |
| Idle credits       | `WAREHOUSE_METERING_HISTORY`     | Idle share of compute credits above 30 percent for a day                 |
| Overload queueing  | `QUERY_HISTORY`                  | Over 10 percent of a warehouse's queries queued for overload in an hour  |
| Remote spilling    | `QUERY_HISTORY`                  | Any query with `BYTES_SPILLED_TO_REMOTE_STORAGE` above zero              |
| Failed queries     | `QUERY_HISTORY`                  | Rate of `EXECUTION_STATUS = 'fail'` above baseline for a service account |
| Long-running query | `QUERY_HISTORY()` table function | A query running longer than your job's expected duration                 |
| Quota near limit   | Resource monitor Notify          | 75 to 90 percent of the credit quota                                     |

Route these the same way as your other alerts. Our guide to [Prometheus Alertmanager](https://last9.io/blog/prometheus-alertmanager/) covers grouping and routing if your Snowflake metrics end up in Prometheus.

## How does Last9 fit with Snowflake monitoring?

Snowflake's usage views tell you what happened inside the warehouse. They don't tell you which API endpoint or pipeline run caused it. Bringing Snowflake metrics into the same place as your application telemetry closes that gap.

The OpenTelemetry Collector's contrib distribution includes a [Snowflake receiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/snowflakereceiver), currently at alpha stability. It queries the ACCOUNT_USAGE schema on a schedule and emits metrics such as `snowflake.query.queued_overload`, `snowflake.queued_overload_time.avg`, `snowflake.query.bytes_spilled.remote.avg` and `snowflake.billing.warehouse.total_credit.total`. A few details from its README matter:

- It needs a warehouse to run its queries on, so those queries use credits.
- The role defaults to ACCOUNTADMIN. Point it at a dedicated monitoring role instead.
- The collection interval defaults to 30 minutes, which fits the latency of the views underneath.
- Several useful metrics, including the billing and spill metrics, are disabled by default and need to be enabled in the `metrics` block.

Export from the Collector to Last9 over OTLP with our [OpenTelemetry Collector integration](https://last9.io/docs/integrations/observability/opentelemetry-collector/). If your services are instrumented with OpenTelemetry, their database spans for Snowflake calls sit beside these metrics, so a slow endpoint and the warehouse queueing behind it show up together.

Labels such as warehouse, database, user and query type add up quickly, and Last9 handles 20M series per metric per day by default, with no sampling. Our [Control Plane](https://last9.io/control-plane/) lets you drop, remap, redact, forward and aggregate metrics, logs and traces at ingest.

For the database metrics that apply beyond Snowflake, see our post on [database monitoring metrics](https://last9.io/blog/database-monitoring-metrics/).

## Watch queues, spills and idle credits

Snowflake already records what you need to monitor it. QUERY_HISTORY shows which query patterns take the most time and why, the queue columns separate a busy warehouse from a sleepy one, and WAREHOUSE_METERING_HISTORY shows where credits go, including the ones spent doing nothing.

Remember the latency of ACCOUNT_USAGE when you build alerts, use INFORMATION_SCHEMA for anything that has to be live, and treat resource monitors as a backstop. When you want Snowflake metrics next to the services that query it, [Last9](https://last9.io/) takes OpenTelemetry and Prometheus data and queries them together.

## FAQ

### How do you monitor Snowflake?

Snowflake records its own usage in the SNOWFLAKE database. The ACCOUNT_USAGE views such as QUERY_HISTORY, WAREHOUSE_LOAD_HISTORY and WAREHOUSE_METERING_HISTORY cover queries, warehouse load and credits for the last year, and INFORMATION_SCHEMA gives a shorter, real-time view. Most teams query these views on a schedule, watch queueing, spilling and credits per warehouse, and add resource monitors as a backstop on spend.

### What is the latency of Snowflake ACCOUNT_USAGE views?

Snowflake's documentation says most ACCOUNT_USAGE views have about 2 hours of latency, with a range of 45 minutes to 3 hours depending on the view. QUERY_HISTORY lags by up to 45 minutes, WAREHOUSE_LOAD_HISTORY and WAREHOUSE_METERING_HISTORY by up to 3 hours, and the CREDITS_USED_CLOUD_SERVICES column by up to 6 hours. INFORMATION_SCHEMA has no latency but keeps less history.

### How do you find slow queries in Snowflake?

Query SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY and sort by TOTAL_ELAPSED_TIME, which is in milliseconds. Grouping by QUERY_PARAMETERIZED_HASH collects runs of the same query with different literal values, so you find the patterns that cost the most time overall. Then check each one for spilling, poor partition pruning and time spent queued.

### What does queued overload time mean in Snowflake?

QUEUED_OVERLOAD_TIME is the time in milliseconds a query spent in the warehouse queue because the warehouse was overloaded by its current workload. It is separate from QUEUED_PROVISIONING_TIME, which is time spent waiting for the warehouse to be created, resumed or resized. Steady overload queueing points to a concurrency problem, which Snowflake recommends handling with multi-cluster warehouses.

### How do you monitor Snowflake credit usage?

WAREHOUSE_METERING_HISTORY in ACCOUNT_USAGE reports credits per warehouse per hour. CREDITS_USED_COMPUTE minus CREDITS_ATTRIBUTED_COMPUTE_QUERIES gives idle credits, the compute billed while a warehouse ran with no queries. Track daily credits per warehouse against a baseline, alert on spikes, and use resource monitors or budgets as a backstop.

### Can a Snowflake resource monitor stop spending at an exact limit?

No. Snowflake's documentation says resource monitors are not intended for setting precise limits on credit usage, and a warehouse can keep consuming credits after the quota is reached. The Suspend action also waits for running statements to finish. Resource monitors work for warehouses only, so serverless features need a budget instead.
