Snowflake Cost Optimization Playbook

Snowflake Cost Optimization: A Practical Playbook with SQL

Snowflake makes it very easy to spend money. Warehouses start in seconds, scale with one click and bill per second. That flexibility is exactly why bills creep up. The good news is that most savings come from a small number of habits.

This playbook walks through how Snowflake charges you, how to find where your credits go, and the fixes that usually pay off, with SQL you can run today.

How Snowflake bills you

  • Compute (virtual warehouses): usually 70–90% of the bill. An X-Small uses 1 credit per hour, and each size up doubles it (Small 2, Medium 4, Large 8 and so on). Billing is per second with a 60-second minimum each time a warehouse resumes.
  • Serverless features: Snowpipe, automatic clustering, search optimisation, materialized views, serverless tasks, replication and Cortex AI functions all consume credits without a warehouse.
  • Cloud services: metadata, compilation and security. Only the part above 10% of your daily warehouse usage is billed.
  • Storage: active data plus Time Travel and Fail-safe copies, billed per TB per month.
  • Data transfer: egress across regions or clouds.

Step 1: Find where the credits go

Start with data, not guesses. These queries use the SNOWFLAKE.ACCOUNT_USAGE views, which need ACCOUNTADMIN or a role granted access to them. Expect up to a few hours of latency in these views.

Credits by warehouse, last 30 days:

SELECT warehouse_name,
       ROUND(SUM(credits_used), 1) AS credits
FROM snowflake.account_usage.warehouse_metering_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY warehouse_name
ORDER BY credits DESC;

Serverless and other services, last 30 days:

SELECT service_type,
       ROUND(SUM(credits_used), 1) AS credits
FROM snowflake.account_usage.metering_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY service_type
ORDER BY credits DESC;

Your most expensive query patterns. QUERY_ATTRIBUTION_HISTORY assigns warehouse credits to individual queries, and grouping by the parameterised hash rolls up repeated queries that only differ by literal values:

SELECT query_parameterized_hash,
       ANY_VALUE(query_id)                    AS sample_query_id,
       ANY_VALUE(warehouse_name)              AS warehouse_name,
       COUNT(*)                               AS runs,
       ROUND(SUM(credits_attributed_compute), 2) AS credits
FROM snowflake.account_usage.query_attribution_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY query_parameterized_hash
ORDER BY credits DESC
LIMIT 20;

Usually a handful of scheduled jobs and dashboards account for most of the spend. Fix those first.

Step 2: Stop paying for idle warehouses

The default auto-suspend is often 10 minutes. For most ETL and BI warehouses, 60 seconds is plenty. The local cache is lost on suspend, so for heavily cached BI workloads 3–5 minutes can be a better balance.

ALTER WAREHOUSE transform_wh SET
  AUTO_SUSPEND = 60
  AUTO_RESUME  = TRUE;

-- Find warehouses with long auto-suspend settings
SHOW WAREHOUSES;
SELECT "name", "size", "auto_suspend"
FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()))
WHERE "auto_suspend" > 300 OR "auto_suspend" IS NULL;

Step 3: Right-size, then scale out instead of up

  • Test one size down. If a job takes 4 minutes on Large and 7 minutes on Medium, Medium is cheaper (28 vs 32 credit-minutes) and probably fast enough.
  • Go up a size only when queries spill. Spilling to remote storage means the warehouse is too small for that query.
  • Use multi-cluster for concurrency, not size. Many BI users running small queries need more clusters, not a bigger warehouse. The ECONOMY scaling policy favours fuller clusters over speed.
-- Queries that spilled in the last 7 days
SELECT query_id, warehouse_name, warehouse_size,
       ROUND(bytes_spilled_to_local_storage  / POWER(1024, 3), 2) AS local_spill_gb,
       ROUND(bytes_spilled_to_remote_storage / POWER(1024, 3), 2) AS remote_spill_gb,
       total_elapsed_time / 1000 AS seconds
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD(day, -7, CURRENT_TIMESTAMP())
  AND bytes_spilled_to_remote_storage > 0
ORDER BY remote_spill_gb DESC
LIMIT 20;

-- Concurrency for BI: scale out, economically
ALTER WAREHOUSE bi_wh SET
  MIN_CLUSTER_COUNT = 1
  MAX_CLUSTER_COUNT = 3
  SCALING_POLICY    = 'ECONOMY';

Step 4: Put guardrails in place

Guardrails turn a surprise bill into an alert. Resource monitors cap warehouse credits, and a statement timeout kills runaway queries.

CREATE OR REPLACE RESOURCE MONITOR monthly_cap WITH
  CREDIT_QUOTA    = 500
  FREQUENCY       = MONTHLY
  START_TIMESTAMP = IMMEDIATELY
  TRIGGERS
    ON 80  PERCENT DO NOTIFY
    ON 100 PERCENT DO SUSPEND
    ON 110 PERCENT DO SUSPEND_IMMEDIATE;

ALTER WAREHOUSE transform_wh SET RESOURCE_MONITOR = monthly_cap;

-- Kill anything running longer than 30 minutes on the BI warehouse
ALTER WAREHOUSE bi_wh SET STATEMENT_TIMEOUT_IN_SECONDS = 1800;

Resource monitors only cover warehouses. For serverless features and AI, use Budgets, and in 2026 Snowflake added per-user AI quotas to cap Cortex and CoWork spend per person per day. Set these before you roll AI features out widely.

Step 5: Make queries do less work

  • Select only the columns you need. Snowflake stores data by column, so SELECT * reads far more than necessary.
  • Filter on columns that prune well, usually dates. Check the query profile: “partitions scanned” should be a small share of “partitions total”.
  • Add clustering keys only to very large tables (multi-TB) with poor pruning on a common filter. Automatic clustering costs credits, so measure the gain.
  • Load incrementally. Rebuilding a whole table every hour is the most common waste I see. Use dynamic tables or streams and tasks instead, as covered in my incremental pipelines tutorial.
  • Try the Query Acceleration Service for warehouses with occasional large scans mixed with small queries. Check first whether a query is eligible:
SELECT PARSE_JSON(SYSTEM$ESTIMATE_QUERY_ACCELERATION('<query_id>'));

ALTER WAREHOUSE bi_wh SET
  ENABLE_QUERY_ACCELERATION = TRUE
  QUERY_ACCELERATION_MAX_SCALE_FACTOR = 4;

Step 6: Trim storage

Storage is cheap per TB, but Time Travel and Fail-safe quietly multiply it for tables that change a lot. Find the worst offenders:

SELECT table_catalog, table_schema, table_name,
       ROUND(active_bytes      / POWER(1024, 3), 1) AS active_gb,
       ROUND(time_travel_bytes / POWER(1024, 3), 1) AS time_travel_gb,
       ROUND(failsafe_bytes    / POWER(1024, 3), 1) AS failsafe_gb
FROM snowflake.account_usage.table_storage_metrics
WHERE deleted = FALSE
ORDER BY time_travel_bytes + failsafe_bytes DESC
LIMIT 20;
  • Use transient tables for staging and scratch data. They have no Fail-safe and at most 1 day of Time Travel.
  • Lower Time Travel retention where you do not need it.
  • Drop tables nobody reads. ACCESS_HISTORY (Enterprise edition) shows which tables have not been queried in months.
CREATE OR REPLACE TRANSIENT TABLE staging.orders_raw (
  order_id   NUMBER,
  payload    VARIANT,
  loaded_at  TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
)
DATA_RETENTION_TIME_IN_DAYS = 0;

Step 7: Watch the serverless and AI line items

  • Automatic clustering and search optimisation run continuously on changing tables. Review them in the metering query from Step 1 and switch off any that no longer pay for themselves.
  • Snowpipe is efficient for steady file arrival, but thousands of tiny files cost more per row. Aim for files of roughly 100–250 MB compressed.
  • Cortex AI functions bill by tokens. Test prompts on a sample before running them across a whole table, and prefer smaller models when quality is good enough.

A 30-minute monthly cost review

  1. Run the warehouse and service-type queries and compare with last month.
  2. Open the top 5 query patterns and check their profiles for full scans and spills.
  3. Check every warehouse has auto-suspend at 60–300 seconds and a resource monitor.
  4. Review Time Travel and Fail-safe on the biggest tables.
  5. Check AI and serverless spend against your budgets.

Do this every month and costs stop being a surprise. The pattern is always the same: measure, fix the top few, and add a guardrail so it cannot happen again.

Further reading

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *