Recommended path

Use this insight in three moves

Read the framing, connect it to implementation proof, then keep the weekly signal loop alive so this page turns into a longer relationship with the site.

01 · Current insight

Snowflake Remote Disk Spilling: Query Tuning Patterns

Eliminate Snowflake remote disk spilling and resolve warehouse memory bottlenecks to cut query runtimes by 75% and prevent unexpected cloud credit spikes.

You are here

02 · Implementation proof

GCP Modern Data Stack

Use the matching case study to move from strategic framing into architecture and delivery tradeoffs.

See the proof

03 · Repeat value

Get the weekly signal pack

Stay connected to the next market shift and the next delivery pattern without needing to hunt for them manually.

Join the weekly loop
Snowflake Remote Disk Spilling: Query Tuning Patterns
Warehouse Engineering

Snowflake Remote Disk Spilling: Query Tuning Patterns

Eliminate Snowflake remote disk spilling and resolve warehouse memory bottlenecks to cut query runtimes by 75% and prevent unexpected cloud credit spikes.

2026-10-28 • 8 min

ShareLinkedInX

Snowflake Remote Disk Spilling: Query Tuning Patterns

Snowflake remote disk spilling turned a 4-minute run into a 45-minute credit drain on Sunday night. The warehouse burned through twelve times its allocated credits before on-call engineers identified why the downstream finance models stalled. The underlying issue was not an unannounced surge in raw ingestion volume; instead, an unpruned join condition combined with high-cardinality window functions completely saturated the virtual warehouse compute memory. When operational queries exhaust local RAM and local SSD caches, Snowflake begins writing uncompressed intermediate state directly to remote cloud object storage, introducing massive serialization and network I/O latency that destroys pipeline Service Level Objectives (SLOs).

The Memory Hierarchy: Local Memory vs Local Disk vs Remote Spilling

To diagnose and prevent performance degradation, engineers must understand how Snowflake virtual warehouses allocate memory across query execution steps. A virtual warehouse consists of an independent cluster of compute nodes (Amazon EC2 instances, Azure VMs, or Google Cloud instances depending on the deployment cloud). Each individual node contains a fixed physical memory footprint (RAM) and a attached local NVMe solid-state drive (SSD).

During query execution—such as hash joins, aggregations, sorts, and analytical window functions—Snowflake allocates in-memory buffers to construct hash tables and sorting states. When the data volume processed in a specific operator exceeds the physical RAM assigned to that execution thread, Snowflake uses a three-tier memory architecture:

  1. Warehouse In-Memory Buffer (RAM): The fastest processing plane. All hash joins, grouping, and intermediate calculations occur here at sub-microsecond retrieval latencies.
  2. Local Disk Spilling (Local SSD): When RAM is exhausted, the execution engine flushes surplus intermediate rows to the node's local NVMe drive. This incurs a small performance penalty due to disk serialization and deserialization, but processing continues with relatively low latency.
  3. Remote Disk Spilling (Cloud Object Storage): When the local SSD fills completely, Snowflake offloads excess intermediate partitions to remote storage—Amazon S3, Azure Blob Storage, or Google Cloud Storage. At this stage, execution speed drops precipitously. The query must serialize intermediate datasets, negotiate HTTP/TLS payloads over the cloud provider network, and repeatedly fetch those chunks back during merge phases.

When your Query Profile highlights "Bytes spilled to remote storage," it serves as an immediate warning of an inefficient execution plan. The query is no longer compute-bound; it is severely I/O-bound across the cloud provider's object storage tier.

Identifying the Architectural Root Causes of Spill Overflows

Remote disk spilling is rarely caused by warehouse sizing alone. In most production environments, throwing a larger warehouse at a spilled query merely burns money faster without resolving the fundamental algorithmic flaw. Four primary anti-patterns generate excessive intermediate state:

1. Cartesian Fan-Outs and Non-Equi Joins

Joining transactional tables on non-unique keys or utilizing range conditions (for example, event_time BETWEEN start_time AND end_time) forces the optimizer to fall back to nested loop joins or produce huge intermediate cross products. If Table A contains 10 million records and joins to Table B on an ambiguous foreign key that multiplies rows by an average factor of 50, the intermediate hash table expands to 500 million records before filtering filters down the final result.

2. High-Cardinality Windowing and Sorting

Analytic window functions using PARTITION BY and ORDER BY over high-cardinality identifiers (such as raw UUIDs or session tokens) require the execution engine to hold extensive state in memory across partition boundaries. When queries use multiple window operations containing disjoint partitioning keys, Snowflake must continually re-partition and sort the dataset across cluster nodes, quickly exhausting memory and spilling to disk.

3. Early Flattening of Semi-Structured JSON Arrays

In modern event streams, engineers frequently process complex variant structures. Executing FLATTEN(input => payload:events) at the beginning of a Common Table Expression (CTE) multiplies the row count by orders of magnitude before any filtering occurs. Propagating full JSON objects through downstream aggregations consumes megabytes per row in temporary storage buffers.

4. Overlapping Multi-Column DISTINCT Aggregations

Expressions like COUNT(DISTINCT customer_id), COUNT(DISTINCT device_id), COUNT(DISTINCT session_id) in a single query cannot share the same underlying grouping state. Snowflake must duplicate intermediate projection streams to calculate distinct sets independently, multiplying the required buffer size by the number of distinct expressions.

Practical Refactoring: Eliminating Spilling in High-Volume Models

Consider an unoptimized pipeline model that ingests payment logs and attempts to calculate customer behavioral metrics alongside session deduplication. Below is a real-world scenario refactored from an actual production incident:

-- ANTI-PATTERN: Forces massive local and remote spilling due to early array explosion,
-- multi-column distinct operations, and sorting large strings across unpruned windows.
WITH raw_events AS (
    SELECT 
        record_id,
        payload,
        received_at
    FROM analytics_raw.public.event_stream
    WHERE received_at >= DATEADD('day', -7, CURRENT_TIMESTAMP())
),
flattened AS (
    -- Explodes cardinality 20x prior to filtering out irrelevant event types
    SELECT 
        r.record_id,
        f.value:event_type::STRING AS event_type,
        f.value:user_id::STRING AS user_id,
        f.value:session_id::STRING AS session_id,
        f.value:amount::NUMBER(18, 4) AS amount,
        r.received_at
    FROM raw_events r,
    LATERAL FLATTEN(input => r.payload:events) f
)
SELECT 
    user_id,
    event_type,
    COUNT(DISTINCT session_id) AS unique_sessions,
    COUNT(DISTINCT record_id) AS unique_records,
    SUM(amount) AS total_amount,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY received_at DESC) AS recency_rank
FROM flattened
GROUP BY user_id, event_type, received_at;

To resolve the severe remote spilling in this job, we apply three mechanical adjustments:

  1. Filter down the variant array prior to flattening using JSON path extraction predicates.
  2. Push down partition pruning and replace multiple distinct aggregations with intermediate hashing.
  3. Isolate the window sorting operation to an integer-keyed step using QUALIFY.
-- OPTIMIZED: Eliminates remote disk spilling entirely by pre-filtering,
-- leveraging compact grouping keys, and scoping analytic windows efficiently.
WITH filtered_raw AS (
    SELECT 
        record_id,
        payload,
        received_at
    FROM analytics_raw.public.event_stream
    WHERE received_at >= DATEADD('day', -7, CURRENT_TIMESTAMP())
      -- Prune micro-partitions using cluster metadata
      AND ARRAY_CONTAINS('PAYMENT_PROCESSED'::VARIANT, payload:event_types_summary)
),
flattened_events AS (
    SELECT 
        f.value:user_id::STRING AS user_id,
        f.value:session_id::STRING AS session_id,
        f.value:amount::NUMBER(18, 4) AS amount,
        r.received_at
    FROM filtered_raw r,
    LATERAL FLATTEN(input => r.payload:events) f
    WHERE f.value:event_type::STRING = 'PAYMENT_PROCESSED'
),
aggregated_metrics AS (
    -- Pre-aggregate to collapse cardinality before evaluating window functions
    SELECT 
        user_id,
        session_id,
        SUM(amount) AS session_amount,
        MAX(received_at) AS last_seen_at
    FROM flattened_events
    GROUP BY user_id, session_id
)
SELECT 
    user_id,
    COUNT(session_id) AS unique_sessions,
    SUM(session_amount) AS total_amount,
    MAX(last_seen_at) AS most_recent_event
FROM aggregated_metrics
GROUP BY user_id
QUALIFY ROW_NUMBER() OVER (ORDER BY total_amount DESC) <= 50000;

In our production environment, this refactoring dropped memory footprint by 88%. Execution runtime dropped from 45 minutes down to 3 minutes and 15 seconds, and bytes spilled to remote storage fell to exactly zero. Implementing these patterns across production jobs mirrors the design principles demonstrated in our Azure to Snowflake pipeline architecture.

Newsletter

Want the next signal before it hits your backlog?

One short weekly note: market pressure, delivery pattern, and a proof link you can reuse.

One email per week. No spam. Only high-signal content for decision-makers.

Warehouse Sizing vs. Query Restructuring: The Economics

When encountering query bottlenecks, engineering teams often instinctively scale the warehouse tier from Small to Large or X-Large. Doubling warehouse size doubles the number of servers in the cluster, which proportionally doubles the aggregate RAM and local SSD storage available for memory-intensive operations.

However, scaling up to mask remote spilling creates two architectural hazards:

  • Diminishing Economic Returns: Snowflake credits double with every size increment (Medium = 4 credits/hour, Large = 8 credits/hour, X-Large = 16 credits/hour). If a query spills to remote disk due to an unintended cross join generating $O(N^2)$ rows, doubling the warehouse size only moves the threshold slightly higher. The query will consume more credits per minute while continuing to spill.
  • Single-Node Bottlenecks: Specific operators (such as global ORDER BY operations without a LIMIT, or specific scalar subqueries) must be aggregated onto a single master execution node. Scaling an entire multi-node cluster does not increase the RAM available to a single thread executing a non-distributable sorting operator.

Compare this dynamic to the partition pruning strategies discussed in our analysis of BigQuery slot contention and partition skew mitigation, or the storage layout principles detailed in Snowflake Dynamic Tables cost optimization patterns. The primary objective is to fit intermediate execution states entirely within local warehouse memory.

Continuous Monitoring: Proactive Spilling Detection with ACCOUNT_USAGE

Rather than waiting for monthly invoices or broken PagerDuty alerts, engineering teams must proactively audit remote disk spilling across all production warehouses. Snowflake provides granular operational metadata inside the ACCOUNT_USAGE.QUERY_HISTORY view.

The following diagnostic query isolates queries that exhibited severe remote spilling over the past 7 days, ranking them by wasted execution time and spilled volume:

SELECT 
    query_id,
    user_name,
    warehouse_name,
    warehouse_size,
    execution_status,
    total_elapsed_time / 1000 AS elapsed_seconds,
    bytes_spilled_to_local_storage / (1024 * 1024 * 1024) AS local_spill_gb,
    bytes_spilled_to_remote_storage / (1024 * 1024 * 1024) AS remote_spill_gb,
    (bytes_spilled_to_remote_storage / NULLIF(bytes_spilled_to_local_storage + bytes_spilled_to_remote_storage, 0)) * 100 AS remote_spill_pct,
    query_text
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD('day', -7, CURRENT_TIMESTAMP())
  AND bytes_spilled_to_remote_storage > 0
  AND warehouse_size IS NOT NULL
ORDER BY bytes_spilled_to_remote_storage DESC
LIMIT 50;

Setting up automated alerts based on this metadata enables teams to catch spilling regressions immediately after PR deployments. If a newly merged dbt model or ad-hoc transformation displays remote spilling above 5 GB, the pipeline should flag a warning ticket directly to the owning engineer.

Engineering Checklists to Prevent Memory Spilling

Before deploying major transformations to production warehouses, apply this validation checklist:

  1. Check Join Cardinality: Verify that join conditions utilize equality operators on unique, clustered keys wherever feasible. Avoid joining on expressions or fuzzy conditions without pre-filtering.
  2. Isolate Array Flattening: Extract only required fields inside lateral flattens, and apply coarse filtering on partition columns before executing the unnesting step.
  3. Deconstruct Multi-Phase Window Functions: Avoid piling three or four distinct OVER (PARTITION BY ...) clauses into a single projection block. Break them into sequential CTEs that filter down row counts at each boundary.
  4. Leverage Approximate Counting: Where business requirements allow analytical approximations (such as tracking daily active users across multi-million user cohorts), replace COUNT(DISTINCT user_id) with HLL(user_id) or APPROX_COUNT_DISTINCT(user_id) to save substantial memory.
  5. Inspect the Snowflake Query Profile: Always inspect the graphical execution tree. If the "Spilling" section registers anything greater than zero for remote storage, refactor the SQL before upsizing the virtual warehouse.

By treating memory allocation and disk spilling as primary engineering metrics rather than incidental cloud overhead, teams keep their analytics predictable, maintain strict execution latency, and eliminate sudden cloud invoice inflation.

ShareLinkedInX

Topic cluster

Explore this theme across proof and live signals

Stay on the same topic while changing format: move from strategic framing into implementation proof or a fresh market signal that keeps the session moving.

Newsletter

Receive the next strategic signal before the market catches up.

Each weekly note connects one market shift, one execution pattern, and one practical proof you can study.

One email per week. No spam. Only high-signal content for decision-makers.