Skip to main content
Snowflake beginner Lesson 2 of 10

Virtual Warehouses: Sizing, Scaling, and Cost

Scale up for one slow query, scale out for many concurrent ones — with credit maths, auto-suspend, and resource monitors that stop a runaway bill.

A virtual warehouse is a cluster of compute you rent by the second. Two independent decisions govern it — how big each cluster is, and how many clusters there are — and mixing them up is the most common way to spend money without getting faster.

Sizes

Each step doubles the compute and doubles the credit rate:

SizeCredits / hourNodes
X-Small11
Small22
Medium44
Large88
X-Large1616
2X-Large … 6X-Large32 … 51232 … 512
alter warehouse bookshop_wh set warehouse_size = large;
+----------------------------------+
| status                           |
|----------------------------------|
| Statement executed successfully. |
+----------------------------------+
1 Row(s) produced. Time Elapsed: 0.204s

Resizing takes effect on the next query and needs no restart. Running queries finish on the old size.

Bigger is often the same price

The instinct that a larger warehouse costs more is only half right. Take a scan-heavy aggregation over a large table:

select
    date_trunc('month', ordered_at) as month,
    country_code,
    sum(amount) as revenue
from orders_large            -- 4.2 billion rows
join customers using (customer_id)
group by 1, 2;
-- warehouse_size = SMALL
+---------+--------------+------------+
| MONTH   | COUNTRY_CODE | REVENUE    |
|---------+--------------+------------|
| ...     | ...          | ...        |
+---------+--------------+------------+
36 Row(s) produced. Time Elapsed: 244.108s
-- warehouse_size = LARGE
36 Row(s) produced. Time Elapsed: 62.755s

Small is 2 credits/hour for 244 seconds → 0.136 credits. Large is 8 credits/hour for 63 seconds → 0.140 credits. Nearly identical cost, and the answer arrived four minutes sooner.

That near-linear scaling holds only while the query can use the extra nodes. Check by looking at whether it spilled:

select
    query_text,
    execution_time / 1000 as seconds,
    bytes_spilled_to_local_storage,
    bytes_spilled_to_remote_storage
from table(information_schema.query_history())
order by start_time desc limit 2;
+------------------------+---------+--------------------------------+---------------------------------+
| QUERY_TEXT             | SECONDS | BYTES_SPILLED_TO_LOCAL_STORAGE | BYTES_SPILLED_TO_REMOTE_STORAGE |
|------------------------+---------+--------------------------------+---------------------------------|
| select date_trunc('... |  62.755 |                              0 |                               0 |
| select date_trunc('... | 244.108 |                    18253611008 |                      2147483648 |
+------------------------+---------+--------------------------------+---------------------------------+

The small warehouse spilled 18 GB to local disk and 2 GB to remote storage. Remote spilling is the single worst thing that can happen to a query’s runtime — it means the working set did not fit in memory and Snowflake fell back to object storage. Any non-zero value in that last column is a signal to size up.

A query that returns one row from a lookup, though, gains nothing:

-- X-Small:  Time Elapsed: 0.412s
-- 2X-Large: Time Elapsed: 0.388s

64× the credit rate for 6% less time. Size to the workload, not to the importance of the dashboard.

Scaling out for concurrency

A bigger warehouse does nothing for twenty analysts running twenty small queries — they queue. That needs more clusters:

alter warehouse reporting_wh set
    warehouse_size = small
    min_cluster_count = 1
    max_cluster_count = 4
    scaling_policy = 'STANDARD';
+----------------------------------+
| status                           |
|----------------------------------|
| Statement executed successfully. |
+----------------------------------+

Snowflake starts extra clusters when queries begin queueing and shuts them down when demand falls. Watch it happen:

select
    warehouse_name,
    avg(avg_running)      as avg_running,
    avg(avg_queued_load)  as avg_queued
from table(information_schema.warehouse_load_history(
    date_range_start => dateadd('hour', -2, current_timestamp())))
group by 1;
+----------------+-------------+------------+
| WAREHOUSE_NAME | AVG_RUNNING | AVG_QUEUED |
|----------------+-------------+------------|
| REPORTING_WH   |        6.42 |       0.03 |
| BOOKSHOP_WH    |        1.10 |       2.87 |
+----------------+-------------+------------+
2 Row(s) produced. Time Elapsed: 0.688s

Read that as a diagnosis. REPORTING_WH runs 6.4 queries concurrently with almost nothing queued — healthy. BOOKSHOP_WH runs 1.1 and queues 2.9 — it needs more clusters, not a bigger size.

SymptomFix
one query is slow, spilling to remotescale up
queries wait in a queuescale out
bothup first, then out

scaling_policy = 'ECONOMY' waits longer before starting a cluster, trading some queueing for fewer credits. Use STANDARD for anything a person is waiting on.

Auto-suspend and the cache trade-off

alter warehouse bookshop_wh set auto_suspend = 60;

Suspending stops the bill, and it also throws away the warehouse’s local disk cache — the copy of recently read micro-partitions held on the cluster’s SSDs. The next query re-reads from object storage:

-- warm cache
Time Elapsed: 1.204s
-- after suspend/resume
Time Elapsed: 8.771s

So the setting is a real trade-off, not a free win:

Workloadauto_suspend
interactive / BI60s
ETL with gaps between steps120-300s
a job that runs hourly, alone60s — the cache is cold anyway
dashboards hitting the same tables all day600s

Never set auto_suspend = 0 or never on a warehouse anyone can forget about. An X-Small left running is 24 credits a day; a 4X-Large is 6,144.

Guarding the bill

create resource monitor bookshop_monthly with
    credit_quota = 500
    frequency = monthly
    start_timestamp = immediately
    triggers
        on 75 percent do notify
        on 90 percent do notify
        on 100 percent do suspend
        on 110 percent do suspend_immediate;

alter warehouse bookshop_wh set resource_monitor = bookshop_monthly;
+-------------------------------------------------+
| status                                          |
|-------------------------------------------------|
| Resource monitor BOOKSHOP_MONTHLY successfully created. |
+-------------------------------------------------+
1 Row(s) produced. Time Elapsed: 0.318s

suspend lets running queries finish; suspend_immediate kills them. Having both, a few percent apart, gives you a grace window before anything is lost.

Add a statement timeout so one bad query cannot run for a day:

alter warehouse bookshop_wh set statement_timeout_in_seconds = 3600;
+----------------------------------+
| status                           |
|----------------------------------|
| Statement executed successfully. |
+----------------------------------+

Where the credits went

select
    warehouse_name,
    sum(credits_used)      as credits,
    round(sum(credits_used) * 3.00, 2) as approx_usd
from snowflake.account_usage.warehouse_metering_history
where start_time >= dateadd('day', -7, current_timestamp())
group by 1
order by credits desc;
+----------------+---------+------------+
| WAREHOUSE_NAME | CREDITS | APPROX_USD |
|----------------+---------+------------|
| ETL_WH         |  412.60 |    1237.80 |
| REPORTING_WH   |   88.14 |     264.42 |
| BOOKSHOP_WH    |   12.03 |      36.09 |
+----------------+---------+------------+
3 Row(s) produced. Time Elapsed: 1.204s

account_usage views lag by up to 45 minutes and keep a year of history; information_schema is current but only keeps 7-14 days. Use the first for billing reviews and the second for debugging what just happened.

Practice

1. Run the same aggregation on X-Small and Large and compute credits for each.
-- X-Small: Time Elapsed: 488.204s  → 1 credit/hr × 488s  = 0.136 credits
-- Large:   Time Elapsed:  62.755s  → 8 credits/hr × 63s  = 0.140 credits

Practically the same spend, four times faster wall-clock. When someone objects that a Large warehouse is “eight times the cost”, this is the arithmetic to show them — the rate is eight times, the bill is not.

2. Find queries that spilled to remote storage.
select query_id, execution_time/1000 as seconds, bytes_spilled_to_remote_storage
from table(information_schema.query_history())
where bytes_spilled_to_remote_storage > 0
order by bytes_spilled_to_remote_storage desc;
+--------------------------------------+---------+---------------------------------+
| QUERY_ID                             | SECONDS | BYTES_SPILLED_TO_REMOTE_STORAGE |
|--------------------------------------+---------+---------------------------------|
| 01b2c3d4-0000-a1b2-0000-c3d400001a2b | 244.108 |                      2147483648 |
+--------------------------------------+---------+---------------------------------+
1 Row(s) produced. Time Elapsed: 0.612s

This query should be the first candidate for a larger warehouse. Remote spilling routinely accounts for most of a slow query’s runtime, so removing it often beats any amount of SQL rewriting.

3. Set max_cluster_count to 3 and check the queue.
+----------------+-------------+------------+
| WAREHOUSE_NAME | AVG_RUNNING | AVG_QUEUED |
|----------------+-------------+------------|
| BOOKSHOP_WH    |        4.88 |       0.11 |
+----------------+-------------+------------+

Queueing dropped from 2.87 to 0.11 without changing the warehouse size. Concurrency problems never respond to a bigger cluster — that is the distinction worth internalising from this lesson.

4. Create a resource monitor with a small quota and check its status.
show resource monitors;
+-------------------+--------------+---------------+-----------+-----------+
| name              | credit_quota | used_credits  | remaining | frequency |
|-------------------+--------------+---------------+-----------+-----------|
| BOOKSHOP_MONTHLY  |          500 |         12.03 |    487.97 | MONTHLY   |
+-------------------+--------------+---------------+-----------+-----------+
1 Row(s) produced. Time Elapsed: 0.194s

A monitor with no triggers clause monitors and never acts, which is a common and expensive misconfiguration. Always attach at least a notify and a suspend trigger.

Next: loading data — stages, COPY INTO, and getting files into tables.

Frequently Asked Questions

What is the difference between scaling up and scaling out in Snowflake?
Scaling up means a larger warehouse size, which gives one query more compute and makes it finish faster. Scaling out means more clusters in a multi-cluster warehouse, which lets more queries run at once without queueing. Slow queries need up; queued queries need out.
Does a bigger Snowflake warehouse cost more?
Per second, yes — each size doubles the credit rate. But it also roughly halves the runtime of a query that can use the extra compute, so the total credits are often about the same and the answer arrives sooner. It costs more only when the query cannot parallelise.
What is auto-suspend and what should I set it to?
The idle period after which a warehouse stops billing. Sixty seconds suits interactive and BI work; a few minutes suits pipelines with gaps between steps. The trade-off is that a resumed warehouse loses its local disk cache, so very short values can make repeated queries slower.
How do I stop a Snowflake bill running away?
Create a resource monitor with a credit quota and attach it to your warehouses, with triggers that notify at a threshold and suspend at the limit. Combine it with statement timeouts so a single runaway query cannot burn a day's budget.