Back to Platform

Snowflake Cost Optimization Without Slowing Teams Down

A practical playbook for reducing Snowflake compute waste by 30-50% while protecting delivery speed and analyst productivity with governance guardrails.

Chris P

Chris P

Platform11 min read
Snowflake cost optimization with governance guardrails: minimalist cover with sizing and spend visuals

Snowflake bills by the second for compute and by the terabyte for storage. Compute dominates most bills at 70 to 85 percent of total spend. The insidious part is that Snowflake's elasticity — the feature that makes it powerful — also makes it easy to waste money. Warehouses auto-resume on every query, scale up on demand, and stay running until they time out. Without governance, costs grow linearly with team size and quadratically with ambition.

This playbook covers five optimization levers that typically reduce compute spend by 30 to 50 percent without slowing teams down: cost attribution for visibility, warehouse right-sizing, workload isolation, resource monitors for guardrails, auto-suspend tuning, and materialization strategy audits. Each lever is independent — start with whichever one addresses your biggest pain point.

Building a Cost Attribution Dashboard

You cannot optimize what you cannot measure. The first step is a cost attribution query that breaks down credit consumption by warehouse, team, pipeline, and query type. Snowflake's ACCOUNT_USAGE schema provides the raw data; you just need to aggregate it usefully.

The key insight is query tagging. If your dbt models and Python scripts set query_tag before executing SQL, you can attribute every credit to a specific pipeline, team, or service. Without tags, you can only attribute costs to warehouses, which is too coarse for meaningful optimization.

sql
1-- Cost attribution dashboard: credits by warehouse, tag, and user2WITH query_credits AS (3  SELECT4    warehouse_name,5    user_name,6    COALESCE(NULLIF(query_tag, ''), 'untagged') AS query_tag,7    DATE_TRUNC('day', start_time)               AS query_date,8    SUM(credits_used_cloud_services)9      + SUM(COALESCE(10          total_elapsed_time / 1000 / 360011          * CASE warehouse_size12              WHEN 'X-Small'  THEN 113              WHEN 'Small'    THEN 214              WHEN 'Medium'   THEN 415              WHEN 'Large'    THEN 816              WHEN 'X-Large'  THEN 1617              WHEN '2X-Large' THEN 3218              WHEN '3X-Large' THEN 6419              WHEN '4X-Large' THEN 12820              ELSE 021            END, 0))                             AS estimated_credits,22    COUNT(*)                                     AS query_count,23    AVG(total_elapsed_time) / 1000               AS avg_duration_sec,24    AVG(bytes_scanned) / POWER(1024, 3)          AS avg_gb_scanned,25    AVG(partitions_scanned)26      / NULLIF(AVG(partitions_total), 0) * 100   AS avg_partition_scan_pct27  FROM snowflake.account_usage.query_history28  WHERE start_time >= DATEADD('day', -30, CURRENT_DATE())29    AND execution_status = 'SUCCESS'30    AND query_type IN ('SELECT', 'INSERT', 'MERGE', 'CREATE_TABLE_AS_SELECT')31  GROUP BY 1, 2, 3, 432)33 34SELECT35  warehouse_name,36  query_tag,37  ROUND(SUM(estimated_credits), 2)     AS total_credits_30d,38  ROUND(AVG(estimated_credits), 2)     AS avg_daily_credits,39  SUM(query_count)                     AS total_queries,40  ROUND(AVG(avg_duration_sec), 1)      AS avg_runtime_sec,41  ROUND(AVG(avg_gb_scanned), 2)        AS avg_gb_scanned,42  ROUND(AVG(avg_partition_scan_pct), 1) AS avg_partition_scan_pct,43  CASE44    WHEN AVG(estimated_credits) > 20 THEN 'optimize immediately'45    WHEN AVG(estimated_credits) > 8  THEN 'review sizing'46    ELSE 'healthy'47  END AS recommendation48FROM query_credits49GROUP BY 1, 250ORDER BY total_credits_30d DESC51LIMIT 50;

Run this query weekly and share the results with engineering and analytics leads. The first run almost always reveals surprises: a dev warehouse running XLARGE because someone forgot to resize it, an ETL pipeline scanning 10x more data than necessary, or a BI dashboard triggering thousands of redundant queries per day.

Implementing Query Tagging

Query tagging is the foundation of cost attribution. Every SQL statement that runs against Snowflake should have a tag identifying who owns it. In dbt, set query tags in your profiles.yml or at the model level. In Python scripts, set the session parameter before executing queries.

sql
1-- Set query tag at the session level (Python, dbt, or any client)2ALTER SESSION SET QUERY_TAG = 'team=analytics;pipeline=fct_revenue;service=dbt';3 4-- In dbt, use the query_tag config in dbt_project.yml:5-- models:6--   marts:7--     +query_tag: 'team=analytics;pipeline=marts;service=dbt'8--   staging:9--     +query_tag: 'team=platform;pipeline=staging;service=dbt'10 11-- Verify tags are being applied12SELECT query_tag, COUNT(*) AS cnt13FROM TABLE(information_schema.query_history(14  dateadd('hour', -1, current_timestamp()),15  current_timestamp()16))17GROUP BY query_tag18ORDER BY cnt DESC;

Use a consistent tag format — we recommend semicolon-separated key=value pairs — so you can parse them programmatically in your attribution dashboard. At minimum, tag every query with team and pipeline. Add service (dbt, airflow, custom-script) and environment (prod, staging, dev) if you need finer granularity.

Warehouse Right-Sizing

Warehouse right-sizing is the highest-impact optimization lever. Snowflake warehouses come in T-shirt sizes from XSMALL (1 credit per hour) to 4XLARGE (128 credits per hour). Each size doubles compute resources and cost. The majority of teams default to LARGE or XLARGE for everything because it is safe — queries run fast and nobody complains about performance.

The reality is that 80 percent of dbt models and ad-hoc queries run perfectly well on SMALL or MEDIUM. Only large aggregations, complex joins across billion-row tables, and heavy data loads benefit from LARGE or above. Every model running on XLARGE that would finish in the same time on SMALL is burning 8x more credits than necessary.

To right-size, query the warehouse metering history for average utilization. If a warehouse's average query execution time does not change meaningfully between SMALL and MEDIUM, use SMALL. Run experiments: downsize a non-critical warehouse for a week, compare runtimes, and promote the change if performance is acceptable.

Workload Isolation

A single shared warehouse for all workloads is the most expensive pattern in Snowflake. BI dashboards, ETL pipelines, and ad-hoc analyst queries have completely different performance profiles and should run on separate warehouses.

BI dashboards need low latency and should never queue. Give them a dedicated warehouse with a short auto-suspend (60 seconds) and economy scaling. ETL pipelines are batch jobs that tolerate queuing but need enough compute to finish within their SLA. Give them a dedicated warehouse with auto-suspend at 120 seconds. Developer and analyst ad-hoc queries are unpredictable and low-priority — give them an XSMALL warehouse with aggressive auto-suspend.

sql
1-- BI warehouse: low latency, fast resume, economy scaling2CREATE WAREHOUSE IF NOT EXISTS wh_bi_prod3  WAREHOUSE_SIZE = 'MEDIUM'4  AUTO_SUSPEND = 605  AUTO_RESUME = TRUE6  MIN_CLUSTER_COUNT = 17  MAX_CLUSTER_COUNT = 38  SCALING_POLICY = 'ECONOMY'9  INITIALLY_SUSPENDED = TRUE10  COMMENT = 'Production BI dashboards. Owner: analytics-team.';11 12-- ETL warehouse: batch throughput, cost-optimized13CREATE WAREHOUSE IF NOT EXISTS wh_etl_batch14  WAREHOUSE_SIZE = 'SMALL'15  AUTO_SUSPEND = 12016  AUTO_RESUME = TRUE17  MIN_CLUSTER_COUNT = 118  MAX_CLUSTER_COUNT = 219  SCALING_POLICY = 'ECONOMY'20  INITIALLY_SUSPENDED = TRUE21  COMMENT = 'Batch ETL pipelines (dbt, Airflow). Owner: platform-team.';22 23-- Dev sandbox: minimal cost, aggressive shutdown24CREATE WAREHOUSE IF NOT EXISTS wh_dev_sandbox25  WAREHOUSE_SIZE = 'XSMALL'26  AUTO_SUSPEND = 6027  AUTO_RESUME = TRUE28  MAX_CLUSTER_COUNT = 129  INITIALLY_SUSPENDED = TRUE30  COMMENT = 'Developer ad-hoc queries. Auto-kills at 60s.';31 32-- Data science warehouse: medium bursts for ML feature engineering33CREATE WAREHOUSE IF NOT EXISTS wh_data_science34  WAREHOUSE_SIZE = 'MEDIUM'35  AUTO_SUSPEND = 12036  AUTO_RESUME = TRUE37  MAX_CLUSTER_COUNT = 238  SCALING_POLICY = 'ECONOMY'39  INITIALLY_SUSPENDED = TRUE40  COMMENT = 'ML feature engineering and model training. Owner: ds-team.';

Enforce warehouse assignment through Snowflake roles. Each team's role should have USAGE privileges only on their designated warehouse. This prevents the common failure mode where an analyst accidentally runs a complex query on the ETL warehouse and consumes credits allocated to the platform team's budget.

Resource Monitors with Credit Quotas

Resource monitors are Snowflake's built-in guardrail against runaway costs. They track credit consumption for a warehouse or account and take action when thresholds are breached: notify, suspend new queries, or kill running queries.

Set resource monitors on every production warehouse. Use weekly quotas based on your expected spend, with notification thresholds at 75 percent and 90 percent. Suspend at 100 percent to prevent budget overruns. This protects against runaway queries, misconfigured pipelines, and the occasional analyst who accidentally cross-joins two billion-row tables.

sql
1-- Resource monitor for the ETL warehouse2CREATE RESOURCE MONITOR IF NOT EXISTS monitor_etl_batch3  WITH CREDIT_QUOTA = 5004  FREQUENCY = WEEKLY5  START_TIMESTAMP = CURRENT_TIMESTAMP()6  TRIGGERS7    ON 75 PERCENT DO NOTIFY8    ON 90 PERCENT DO NOTIFY9    ON 100 PERCENT DO SUSPEND;10 11ALTER WAREHOUSE wh_etl_batch12  SET RESOURCE_MONITOR = monitor_etl_batch;13 14-- Resource monitor for the BI warehouse15CREATE RESOURCE MONITOR IF NOT EXISTS monitor_bi_prod16  WITH CREDIT_QUOTA = 30017  FREQUENCY = WEEKLY18  START_TIMESTAMP = CURRENT_TIMESTAMP()19  TRIGGERS20    ON 75 PERCENT DO NOTIFY21    ON 90 PERCENT DO NOTIFY22    ON 100 PERCENT DO SUSPEND_IMMEDIATE;23 24ALTER WAREHOUSE wh_bi_prod25  SET RESOURCE_MONITOR = monitor_bi_prod;26 27-- Account-level monitor as a safety net28CREATE RESOURCE MONITOR IF NOT EXISTS monitor_account29  WITH CREDIT_QUOTA = 200030  FREQUENCY = MONTHLY31  START_TIMESTAMP = CURRENT_TIMESTAMP()32  TRIGGERS33    ON 80 PERCENT DO NOTIFY34    ON 95 PERCENT DO NOTIFY35    ON 100 PERCENT DO SUSPEND;36 37ALTER ACCOUNT SET RESOURCE_MONITOR = monitor_account;

One important distinction: SUSPEND prevents new queries from starting but lets running queries finish. SUSPEND_IMMEDIATE kills running queries as well. Use SUSPEND for ETL warehouses (a few extra minutes of queries will not break the budget) and SUSPEND_IMMEDIATE for BI warehouses where a runaway query could consume credits rapidly.

Auto-Suspend Tuning

Auto-suspend controls how many seconds a warehouse stays running after its last query finishes. The default is 600 seconds (10 minutes). This means a warehouse that runs one query every 15 minutes stays active for 10 minutes, sits idle for 5 minutes, shuts down, then immediately resumes — paying for 10 minutes of idle compute every cycle.

For BI warehouses that receive frequent but small queries, set auto-suspend to 60 seconds. The warehouse will shut down quickly between bursts and resume in under a second when the next query arrives. For ETL warehouses that run batch jobs with predictable timing, 120 seconds is enough to cover the gap between sequential models. For dev warehouses, 60 seconds minimizes waste from forgotten sessions.

Do not set auto-suspend to 0 (never suspend) unless you have a warehouse that truly runs queries 24/7. Even high-traffic BI warehouses have quiet periods at night where they should suspend.

Materialization Strategy Audit

The final optimization lever is auditing how dbt materializes models. Tables, views, incremental models, and ephemeral CTEs have dramatically different cost profiles.

Tables are rebuilt from scratch on every dbt run. If a table is rebuilt daily but only queried weekly, it is wasting compute on five unnecessary rebuilds per week. Convert it to a view — zero build cost, and the underlying query executes only when the view is read.

Views have no build cost but execute the full query every time they are read. If a view is queried thousands of times per day by a dashboard, it is wasting compute by re-executing the same query repeatedly. Convert it to a table with a clustering key.

Incremental models are the sweet spot for large fact tables — they build quickly by processing only new data, and they serve reads efficiently because the data is pre-computed. But they require correct lookback windows and merge strategies (see the dbt incremental models article for details).

Run this audit quarterly: list every model in your dbt project, its materialization strategy, its build frequency, and its query frequency. Models where build frequency greatly exceeds query frequency should be views. Models where query frequency greatly exceeds build frequency should be tables. Models where both are high should be incremental.

Cost optimization is not a one-time project. Build the attribution dashboard, review it weekly, and treat credit consumption as a product metric alongside data freshness and pipeline reliability. The teams that keep Snowflake costs under control are the ones that make cost visible, assign it to owners, and set guardrails that prevent accidental waste without blocking legitimate work.

Clustering Keys for Scan Efficiency

Clustering keys are Snowflake's mechanism for physically organizing data within micro-partitions. When a table is clustered on a column, Snowflake groups rows with similar values together. This dramatically reduces the number of micro-partitions scanned for filtered queries, which directly reduces compute time and cost.

Add clustering keys to any table where the primary query pattern filters on a specific column. Date columns are the most common clustering key for fact tables. For dimension lookups, cluster on the join key.

sql
1-- Add clustering to high-scan-cost tables2ALTER TABLE analytics.fct_orders3  CLUSTER BY (ordered_at::DATE);4 5ALTER TABLE analytics.fct_events6  CLUSTER BY (event_ts::DATE, event_type);7 8-- Monitor clustering effectiveness9SELECT10  TABLE_NAME,11  CLUSTERING_KEY,12  TOTAL_CONSTANT_PARTITION_COUNT,13  TOTAL_PARTITION_COUNT,14  ROUND(TOTAL_CONSTANT_PARTITION_COUNT / TOTAL_PARTITION_COUNT * 100, 1)15    AS clustering_pct16FROM TABLE(information_schema.automatic_clustering_history(17  date_range_start => dateadd('day', -7, current_date()),18  date_range_end => current_date()19))20ORDER BY TOTAL_PARTITION_COUNT DESC;

Clustering is not free — Snowflake runs automatic reclustering in the background, which consumes credits. But for tables scanned frequently with predictable filter patterns, the read savings far outweigh the reclustering cost. A well-clustered 500 GB fact table might scan 5 GB instead of 500 GB for a date-filtered query, reducing that query's cost by 99 percent.

Building a Weekly Review Cadence

The most effective cost optimization practice is not a tool or a query — it is a weekly 30-minute review meeting between platform engineering and analytics leads. Pull up the cost attribution dashboard, review the top 10 credit consumers, and ask three questions for each: is this workload still necessary, is the warehouse correctly sized, and can the query be optimized?

Track total weekly credits as a trend line. Set a target (for example, flat credits despite growing data volume) and hold the team accountable. Celebrate wins when someone right-sizes a warehouse and saves 200 credits per week. This cultural reinforcement is what turns cost optimization from a one-time project into an ongoing discipline that compounds over quarters.

Tags

Platformsnowflakecost optimizationdata platformwarehouse sizingresource monitorsquery tagging

Related articles

Found this useful? Share it with your team.