Skip to main content

Command Palette

Search for a command to run...

Snowflake Cost Optimization: A Practical Playbook for Data Engineers

Updated
7 min read
Snowflake Cost Optimization: A Practical Playbook for Data Engineers

You inherited a Snowflake environment or built one from scratch and at some point someone asked you: "Are we spending too much on this?"

If you're a solo data engineer or a small team running a production dbt project, that question lands on you. Not just the infrastructure side, but the strategy behind it. This post is the playbook I wish I had when I started optimizing our Snowflake setup: what we changed, why, and what actually moved the needle.

The Setup

Before diving into the optimizations, here's the environment we're working with:

  • Hundreds of dbt models across a multi-tenant schema architecture

  • An orchestration tool for pipeline scheduling

  • Three dedicated warehouses, each with a specific job:

    • DBT_WH — exclusive to dbt builds

    • A medium warehouse for overnight ELT pipelines

    • A medium warehouse for the web application layer (read-only queries)

Separating warehouses by workload type is the foundational decision that makes everything else possible. If you're still running dbt builds and app queries on the same warehouse, that's the first thing to fix.


Optimization 1: Right-Size Your dbt Warehouse

This was the single highest-impact change we made.

As our model count scaled into the hundreds ahead of our production go-live, we revisited warehouse sizing rather than waiting for performance problems to surface in production. We had been running dbt builds on an X-Small warehouse during development, fine for early-stage work, but not built for the full DAG. After upgrading to a Medium warehouse, build time dropped from 2 hours and 15 minutes to just over 1 hour — a ~55% reduction, and that's before production query load is even a factor.

That's not just a developer experience win. In Snowflake's credit model, a Medium warehouse costs 4x more per hour than an X-Small, but if it cuts your runtime by more than 75%, you can actually come out ahead on credits. In our case, the faster build meant less total compute time consumed, even at the higher per-hour rate.

The lesson: don't assume smaller is cheaper. Model the credits consumed, not just the warehouse size.

How to think about it

Warehouse Size Credits/hr Our Build Time Total Credits
X-Small 1 2.25 hrs ~2.25
Medium 4 1.0 hr ~4.0

In our case the Medium is more expensive in absolute credits, but the build finishing an hour earlier means pipelines downstream can start sooner, data freshness improves, and the warehouse auto-suspends faster. The full picture matters.


Optimization 2: Increase dbt Threads

Warehouse size alone isn't enough if dbt is only building one model at a time.

By default, dbt uses a conservative thread count. We bumped ours to 8 threads, meaning up to 8 models can build in parallel at any given moment. Combined with the Medium warehouse (which has more compute to actually run those concurrent queries), this is what drove the most dramatic build time improvement.

In your profiles.yml or Snowflake connection config:

my_project:
  target: prod
  outputs:
    prod:
      type: snowflake
      threads: 8
      # ... rest of config

A good rule of thumb: threads should scale with your warehouse size. X-Small can handle 4 comfortably; Medium can handle 8–12 depending on your model complexity.


Optimization 3: Intermediate Model Materialization

Not all models should be tables but not all should be views either. The real work is figuring out which is which.

We audited our DAG and assigned materializations based on how each layer is actually used, not just convention. The result is a four-layer strategy:

  • Staging: Views. Lightweight source transformations, no need to persist them.

  • Intermediate: Tables — selectively. Only models referenced by multiple downstream models get materialized as tables. If an intermediate model feeds 8 marts, Snowflake would recompute it 8 times as a view, materializing it once eliminates that redundancy. Low fan-out intermediates stay as views.

  • Marts: Tables. The core business logic layer, fully materialized for reliability and query performance.

  • App consumption layer: Views built on top of marts, scoped specifically for the web application. No extra build cost, they inherit the performance of the underlying mart tables and stay easy to adjust without a full rebuild.

The rule of thumb: materialize where the fan-out is, and where the app reads. Everything in between stays as light as possible.

In dbt_project.yml, the default config — with individual model-level overrides for high fan-out intermediates:

In dbt_project.yml:

models:
  my_project:
    staging:
      +materialized: view
    intermediate:
      +materialized: view  # selectively overridden to table at the model level
    marts:
      +materialized: table
    app:
      +materialized: view

Optimization 4: Clustering Keys on Large Tables

For our largest fact tables, ones being queried repeatedly by the web app, we added clustering keys in Snowflake.

Without clustering, Snowflake scans the full micro-partition set when filtering. With clustering on the right columns (typically date fields and high-cardinality join keys), queries scan fewer partitions and return faster, consuming fewer credits.

In dbt, this is a model-level config:

{{ config(
    materialized='table',
    cluster_by=['client_id', 'effective_date']
) }}

A few things to keep in mind:

  • Clustering is most valuable on tables over ~1TB or with very frequent, filter-heavy queries

  • Snowflake automatically maintains clustering over time (Automatic Clustering), which has its own cost, monitor it

  • Not every table needs it, be selective


Optimization 5: Dedicated Warehouses Per Workload

We touched on this earlier, but it's worth spelling out explicitly.

Running everything on one warehouse creates two problems:

  1. Contention: dbt builds compete with app queries, slowing both down

  2. Blurry cost attribution: you can't tell what's expensive if it all runs on one warehouse

Our three-warehouse setup maps cleanly to three workload types:

Warehouse Workload Size Auto-Suspend
DBT_WH dbt model builds Medium 60 seconds
PIPELINE_WH Overnight ELT, Prefect flows Medium 60 seconds
APP_WH Web app read queries Medium 10 seconds

The app warehouse gets a shorter auto-suspend because queries are bursty and infrequent, no reason to keep it warm. The build and pipeline warehouses stay active longer during their scheduled windows.


What We Didn't Do (and Why)

A few things that often come up in cost optimization discussions that we intentionally skipped:

  • Query result caching: Snowflake does this automatically, no action needed

  • Resource monitors: Useful for hard spend caps, but we rely on warehouse-level visibility instead

  • Multi-cluster warehouses: Overkill for our scale; adds cost without benefit unless you have heavy concurrency spikes


The Bottom Line

Cost optimization in Snowflake isn't one big move, it's a series of intentional decisions that compound. For us, the highest-leverage changes were:

  1. Right-sizing the dbt warehouse (X-Small → Medium)

  2. Increasing threads to 8 for parallel builds

  3. Reclassifying intermediate models as tables or views

  4. Adding clustering keys to high-query-volume tables

  5. Separating workloads into dedicated warehouses

The result: build times cut by ~55%, cleaner cost attribution by workload, and a data stack that's easier to debug when something goes wrong.

If you're running dbt on Snowflake and haven't revisited these settings since initial setup, start with threads and warehouse sizing. The ROI is immediate and the changes take about 10 minutes to implement.


Have questions about your specific setup or want to compare notes? Drop a comment below, always happy to talk data stack.