Home DevOps Airflow + Snowflake: Queued Queries and the 45-Minute Wait
Advanced 4 min · August 1, 2026
Airflow Snowflake Integration

Airflow + Snowflake: Queued Queries and the 45-Minute Wait

Airflow DAGs queued against Snowflake for 45 minutes when the warehouse suspended.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow DAG basics and the TaskFlow API.
  • A Snowflake account with a warehouse and a service user.
  • Basic SQL and an understanding of ELT vs ETL.
  • The apache-airflow-providers-snowflake package installed.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • SnowflakeOperator runs a SQL statement on a warehouse using a connection, with retries and logging handled by Airflow.
  • The connection carries account, user, warehouse, database, and schema — one place to configure where queries execute.
  • ELT beats ETL here: load raw data first, transform inside Snowflake with SQL or dbt, not in the DAG.
  • A suspended warehouse queues every query until auto-resume kicks in; our run waited 45 minutes in QUEUED state.
  • Right-size the warehouse to the job and set auto-suspend aggressively; an X-Small idling burns credits every minute.
  • Operator-level parameters (warehouse, database, schema, role) override the connection's, and SnowflakeOperator is deprecated in favor of the provider's SQLExecuteQueryOperator.
✦ Definition~90s read
What is Airflow Snowflake Integration?

Airflow + Snowflake integration is the pattern of orchestrating warehouse work — COPY loads, SQL transforms, and assertions — through SnowflakeOperator, SnowflakeHook, and a dedicated connection.

Your DAG is a courier and the Snowflake warehouse is the loading dock.
Plain-English First

Your DAG is a courier and the Snowflake warehouse is the loading dock. When the dock is closed (suspended), the courier doesn't fail — it waits in line with every other courier until the dock opens. If the dock is tiny and the trucks are many, the line grows and the wait stretches. The fix is a dock that opens automatically when work arrives (auto-resume), sized for the trucks you actually send (warehouse sizing), and a dispatcher who knows which dock to use (the connection).

The nightly ELT DAG ran fine. Every task turned green. It just started 45 minutes late, every single night, and nobody noticed until the dashboard numbers stopped matching the morning standup.

Forty-five minutes of queuing against a Snowflake warehouse that wasn't there when the first query landed. Not an error, not a failure — a queue. That's the most dangerous way Snowflake DAGs break: quietly, politely, and on a schedule you can't see in the task logs.

The fix is a contract, not a prayer: a dedicated warehouse with auto-resume, wired through the connection, monitored by QUEUED state. This article builds that contract from the connection up — SnowflakeOperator, the ELT pattern, warehouse sizing, and the cost controls that keep the bill boring.

1. The Airflow → Snowflake Connection Anatomy

The connection is the contract between Airflow and Snowflake. It holds account, user, password or key, warehouse, database, and schema — everything a query needs to know about where it runs. Get the warehouse here first: it's the default every operator and hook inherits.

Create it once, reference the id forever. The DAG never touches credentials, and changing the warehouse for an environment is a config change, not a code change.

Two fields are the classic traps: the account identifier must include the region and cloud provider (company.us-east-1, not company), and the schema path matters if your ELT writes to a specific database.

``bash airflow connections add snowflake_prod \ --conn-type snowflake \ --conn-host company.us-east-1 \ --conn-login etl_airflow \ --conn-extra '{"warehouse": "ETL_WH", "database": "ANALYTICS", "schema": "RAW", "role": "ETL_ROLE"}' ``

For production, pull the password from a secrets backend instead of passing it on the command line.

🔥Connection = Defaults for Every Operator
Whatever you set in the connection is what every SnowflakeOperator and SnowflakeHook uses unless the task overrides it. Set warehouse, database, and schema once; keep operator-level overrides for exceptions only.
📊 Production Insight
The connection is the default contract for all Snowflake tasks.
Account identifiers need region and cloud provider.
Environment differences are config changes, not code.
🎯 Key Takeaway
One connection, one source of truth for Snowflake config.
Warehouse, database, and schema belong in it.
Task-level overrides stay the exception, not the rule.
The Snowflake Connection Contract The Snowflake Connection Contract One connection, one source of truth for every task Airflow DAG tasks inherit connection defaults Connection: snowflake_prod account company.us-east-1 user etl_airflow warehouse ETL_WH database ANALYTICS schema RAW Snowflake Warehouse — ETL_WH defaults for every operator and hook THECODEFORGE.IO
thecodeforge.io
Airflow Snowflake Integration

2. SnowflakeOperator Basics

SnowflakeOperator is a task that runs one SQL statement: run this SQL, on this warehouse, with this connection, retried like any Airflow task. Its output is the query's row count or result set — handy for the next task to consume.

It's the right tool for standard steps: create tables, truncate partitions, run stored procedures, load from stage. For anything with Python logic around the SQL, reach for SnowflakeHook inside a task instead.

The operators in the snowflake provider also handle a lot of the state for you: running, failed, and retried queries are all tracked at the task level, so the 45-minute queue incident shows up as a long-running task — visible, if you look.

Two precedence rules matter in production. Any parameter passed to the operator — warehouse, database, schema, role — takes priority over the same field in the connection, so the connection stays the default and the operator handles the exceptions. And a sql argument holding multiple statements needs split_statements=True (or pass a list of SQL strings), while parameters={...} binds values into the statement.

One deprecation heads-up from the provider docs: SnowflakeOperator is deprecated and will be removed in a future provider version — the stable docs now document SQLExecuteQueryOperator for the same job, with identical connection metadata and the same argument-over-connection precedence. Plan the migration, and know the check operators (SnowflakeCheckOperator, SnowflakeValueCheckOperator) for in-warehouse assertions instead of hand-rolled SQL checks.

snowflake_operator_dag.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from datetime import datetime, timedelta

from airflow.decorators import dag
from airflow.providers.snowflake.operators.snowflake import SnowflakeOperator


@dag(
    schedule="@daily",
    start_date=datetime(2026, 7, 1),
    catchup=False,
    default_args={
        "retries": 2,
        "retry_delay": timedelta(minutes=5),
        "snowflake_conn_id": "snowflake_prod",
    },
)
def refresh_marts():
    build_orders = SnowflakeOperator(
        task_id="build_orders_mart",
        sql="""
            CREATE OR REPLACE TABLE ANALYTICS.MARTS.ORDERS_DAILY AS
            SELECT DATE(order_ts) AS day, COUNT(*) AS orders, SUM(amount) AS revenue
            FROM ANALYTICS.RAW.ORDERS
            WHERE DATE(order_ts) = '{{ ds }}'
            GROUP BY 1
        """,
        warehouse="ETL_WH",
    )

    build_orders


refresh_marts()
Output
build_orders_mart succeeded; 0 rows returned (DDL statement).
📊 Production Insight
SnowflakeOperator runs one SQL statement per task.
Retries and logging are first-class Airflow features.
Long task duration is the first QUEUED symptom to watch.
Operator parameters outrank connection defaults; plan the SQLExecuteQueryOperator migration.
🎯 Key Takeaway
SQL steps get operators; logic steps get hooks.
The warehouse and connection come from defaults or overrides.
Duration spikes are the visible face of hidden queues.
SnowflakeOperator: One SQL Per Task SnowflakeOperator: One SQL Per Task SQL, warehouse, connection, retries — all Airflow-managed SnowflakeOperator — build_orders_mart retries 2 · delay 5 min sql CREATE OR REPLACE TABLE ORDERS_DAILY warehouse ETL_WH — explicit override connection snowflake_prod — defaults Output: row count or result set 0 rows returned — DDL statement 45-min queue = long-running task visible in the Grid view, if you look THECODEFORGE.IO
thecodeforge.io
Airflow Snowflake Integration

3. ELT: Load Raw, Transform In-Warehouse

ETL pulls transformation into Python; ELT loads raw and transforms inside the warehouse with SQL or dbt. For Airflow + Snowflake, ELT wins almost every time: Snowflake is the fastest engine for this shape of work, and the DAG stays thin — load, then run SQL, then done.

The pattern: one task copies raw data into a landing table, a second runs the transforms as SQL statements, and a third asserts row counts. The Python in the DAG shrinks to orchestration.

The hook reuse matters: every task in this DAG shares the snowflake_prod connection, so the warehouse, database, and schema stay consistent across the whole run. No drift, no 'why is this task in the wrong schema' mysteries.

elt_dag.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from datetime import datetime, timedelta

from airflow.decorators import dag, task
from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
from airflow.providers.snowflake.operators.snowflake import SnowflakeOperator


@dag(
    schedule="@daily",
    start_date=datetime(2026, 7, 1),
    catchup=False,
    default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
)
def elt_orders():
    load_raw = SnowflakeOperator(
        task_id="load_raw_orders",
        snowflake_conn_id="snowflake_prod",
        sql="""
            COPY INTO ANALYTICS.RAW.ORDERS
            FROM @ETL_STAGE/orders_{{ ds }}/
            FILE_FORMAT = (TYPE = 'CSV', SKIP_HEADER = 1)
        """,
        warehouse="ETL_WH",
    )

    transform_orders = SnowflakeOperator(
        task_id="transform_orders",
        snowflake_conn_id="snowflake_prod",
        sql="""
            INSERT INTO ANALYTICS.MARTS.ORDERS_DAILY
            SELECT DATE(o.order_ts) AS day, COUNT(*) AS orders, SUM(o.amount) AS revenue
            FROM ANALYTICS.RAW.ORDERS o
            WHERE DATE(o.order_ts) = '{{ ds }}'
            GROUP BY 1
        """,
        warehouse="ETL_WH",
    )

    @task
    def assert_loaded() -> int:
        hook = SnowflakeHook(snowflake_conn_id="snowflake_prod", warehouse="ETL_WH")
        count = hook.get_first("SELECT COUNT(*) FROM ANALYTICS.MARTS.ORDERS_DAILY WHERE day = '{{ ds }}'")[0]
        if count == 0:
            raise ValueError("No rows loaded for the current interval")
        return count

    load_raw >> transform_orders >> assert_loaded()


elt_orders()
Output
load_raw_orders: copied 48,120 rows; transform_orders: inserted 48,120 rows; assert_loaded: 48120.
📊 Production Insight
Load raw, transform in-warehouse, assert after.
The DAG stays thin; Snowflake does the heavy lifting.
One connection id keeps every task on the same contract.
🎯 Key Takeaway
ELT beats ETL for the warehouse-native stack.
COPY for raw, SQL for transforms, a check to close it.
Hook reuse is consistency you don't have to maintain.
ELT: Load Raw, Transform In-Warehouse ELT: Load Raw, Transform In-Warehouse ETL pulls logic into Python; ELT keeps the DAG thin ETL — Transform in Python DAG carries the logic ELT — Transform in Warehouse DAG stays thin 1 · load_raw_orders — COPY INTO RAW.ORDERS from @ETL_STAGE, CSV 2 · transform_orders — SQL INSERT MARTS.ORDERS_DAILY · GROUP BY 3 · assert_loaded — row count check raises when COUNT = 0 All tasks share snowflake_prod no schema drift across the run THECODEFORGE.IO
thecodeforge.io
Airflow Snowflake Integration

4. Warehouse Auto-Suspend vs DAG Timing

Warehouses suspend after idle time and resume on demand — unless auto-resume is off, in which case the first query queues until someone or something starts the warehouse. That queue is exactly the 45-minute incident.

The timing trap: auto-resume is not instant. A suspended warehouse takes seconds to start, and every query that arrives during startup waits its turn. Nightly DAGs that fire into a cold warehouse pay this tax every run, silently.

The contract: the DAG's connection names a warehouse with AUTO_RESUME = TRUE and a short AUTO_SUSPEND — long enough to not thrash between tasks in one run, short enough to not burn credits overnight. Then the DAG's schedule becomes the only timing variable left.

``sql CREATE WAREHOUSE IF NOT EXISTS ETL_WH WITH WAREHOUSE_SIZE = 'MEDIUM' AUTO_SUSPEND = 60 AUTO_RESUME = TRUE INITIALLY_SUSPENDED = TRUE; ``

⚠ Auto-Resume Is a Queue, Not a Magic Button
Resuming a suspended warehouse takes real seconds, and queued queries wait in the meantime. Design the DAG for cold starts: set auto-resume on, keep the warehouse warm for sequential tasks with AUTO_SUSPEND=60, and alert on QUEUED time.
📊 Production Insight
Auto-resume is a queue with a startup delay.
Cold warehouses tax every nightly run silently.
AUTO_SUSPEND=60 keeps runs warm without burning credits.
🎯 Key Takeaway
Warehouse state is part of the DAG's timing contract.
Enable auto-resume; set auto-suspend deliberately.
The schedule should be the only timing variable left.

5. S3ToSnowflakeOperator for Bulk Loads

When data lands in S3, the amazon provider's S3ToSnowflakeOperator loads it straight into Snowflake in one task: it reads the S3 key, stages it, and runs COPY INTO with your file format and stage settings.

The value is a full Airflow task wrapped around a data transfer — retries, logging, and lineage for free. And it removes the download-reupload hop most teams add by hand.

Watch the connection pair: this operator needs both an S3 connection (aws) and the Snowflake connection, so both must be configured in the same environment. The warehouse still comes from the Snowflake connection, so the sizing contract from section 4 still applies.

s3_to_snowflake_dag.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from datetime import datetime, timedelta

from airflow.decorators import dag
from airflow.providers.amazon.aws.transfers.s3_to_snowflake import S3ToSnowflakeOperator


@dag(
    schedule="@daily",
    start_date=datetime(2026, 7, 1),
    catchup=False,
    default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
)
def load_from_s3():
    load_events = S3ToSnowflakeOperator(
        task_id="s3_events_to_snowflake",
        s3_keys=["events/{{ ds }}/events.json"],
        s3_bucket="analytics-ingest-prod",
        table="ANALYTICS.RAW.EVENTS",
        stage="ETL_STAGE",
        file_format="(TYPE = 'JSON')",
        aws_conn_id="aws_prod",
        snowflake_conn_id="snowflake_prod",
    )

    load_events


load_from_s3()
Output
s3_events_to_snowflake succeeded; 128,540 rows copied from events/2026-08-01/events.json.
📊 Production Insight
One operator covers the whole S3-to-warehouse hop.
Retries, logging, and lineage come with the task.
Both connections must exist in the target environment.
🎯 Key Takeaway
S3ToSnowflakeOperator is bulk ingest in one task.
It removes the manual staging dance entirely.
Connection pairs need checking per environment.

6. Cost Controls: Warehouses, Retries, Schedule Discipline

Snowflake bills per credit per active second. The cost levers for an Airflow workload: warehouse size, auto-suspend timeout, retry behavior, and what the schedule actually runs.

Right-size the warehouse to the job, not to the fear. An X-Small that finishes in 20 minutes costs one credit; an X-Large idling through the same job costs sixteen. Watch QUEUED time before sizing up — a queue is usually a concurrency problem, and the fix is a second cluster or a separate warehouse, not a bigger one.

Retries multiply cost: each retry re-runs the query and may spin the warehouse back up. Keep retries for transient resume errors, set retry_delay so the warehouse can warm, and use a resource monitor with a hard cap so a runaway DAG can't spend the quarter.

📊 Production Insight
Credits burn per active second, per cluster.
Right-size by watching QUEUED time, not fear.
A resource monitor caps what a bad DAG can spend.
🎯 Key Takeaway
Cost is size times active time times retries.
Sizing up is rarely the fix for a queue.
Hard caps make runaway DAGs boring incidents.

7. Monitoring QUEUED State

The 45-minute incident hid because nobody watched the queue. Task success was green; the queue was invisible. The fix: a query that measures QUEUED time per query, checked like any other metric.

The cleanest query pulls from SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY: elapsed, queued, and executed times per query. Any query with queued_overload_time above a threshold — 60 seconds is a good starting alarm — gets a ticket.

Make it a habit, not an incident response: a weekly review of QUEUED time per warehouse tells you when to resize, when to split warehouses, and when your schedule is outrunning the warehouse's sleep cycle.

queued_check.sqlSQL
1
2
3
4
5
6
7
8
9
10
SELECT
    query_id,
    warehouse_name,
    user_name,
    start_time,
    queued_overload_time AS queued_ms
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE start_time >= DATEADD(day, -1, CURRENT_TIMESTAMP())
  AND queued_overload_time > 60000
ORDER BY queued_overload_time DESC;
Output
query_id | warehouse_name | queued_ms
8f2c91.. | ETL_WH | 2713045 (45 min, the incident signature)
💡Queue Alerts Over Task Alerts
A Snowflake task can be green while its query queued for 45 minutes. Alert on QUERY_HISTORY.queued_overload_time, not just Airflow task state. Sixty seconds is a sane first threshold.
📊 Production Insight
Queued time is the metric that catches this class of bug.
QUEUED-overload queries are the incident signature.
Watch it weekly and resize before incidents, not after.
🎯 Key Takeaway
Task state never shows warehouse queuing.
Query the queue; alert on queued_overload_time.
Weekly queue review is the cheapest resize tool.
● Production incidentPOST-MORTEMseverity: high

The 45-Minute Queue

Symptom
The nightly ELT DAG ran 45 minutes late: every SnowflakeOperator task sat in QUEUED state in the Grid view while the warehouse showed as suspended.
Assumption
The team assumed Snowflake would resume the warehouse instantly on the first query, so the DAG didn't set a warehouse or manage state.
Root cause
The DAG's connection had no warehouse set, so queries fell to a suspended shared warehouse. Auto-resume lag, plus a small warehouse sizing up while queued work waited, held every query until the warehouse was fully up.
Fix
Created a dedicated ETL warehouse with auto_resume enabled, set it in the Snowflake connection, switched the DAG to SnowflakeOperator with an explicit warehouse override, and started monitoring QUEUED time via query_history.
Key lesson
  • A suspended warehouse is a queue, not an error — your DAG 'runs' while queries wait.
  • Set the warehouse in the connection or operator; never rely on defaults.
  • Auto-resume and warehouse sizing belong in the DAG's contract, not the warehouse's mood.
  • Watch QUEUED time, not just task success, when debugging Snowflake DAGs.
Production debug guideSymptom to Action5 entries
Symptom · 01
Tasks sit in QUEUED for minutes
Fix
Check warehouse state with SHOW WAREHOUSES; if suspended, enable AUTO_RESUME and set the warehouse explicitly in the connection.
Symptom · 02
Errors say the warehouse does not exist
Fix
The connection or operator references a warehouse the account doesn't have; verify the name case-sensitively.
Symptom · 03
Queries fail with code 003001 after resume
Fix
A restarting warehouse briefly rejects queries; SnowflakeOperator retries handle it, so keep retries and retry_delay set.
Symptom · 04
Tasks succeed but the target table is empty
Fix
The ELT ran in the wrong database or schema; confirm database and schema in the connection or operator.
Symptom · 05
Credit spike on the ETL warehouse
Fix
Auto-suspend is too loose or the warehouse is oversized for the query; right-size and set AUTO_SUSPEND to a few minutes.
Integration Approaches for Airflow + Snowflake
AspectSnowflakeOperatorSnowflakeHook + PythonDirect SDK in a Task
SQL visibilityIn the operator, visible in task codeIn your Python, anywhereIn your Python, anywhere
Retries and loggingFirst-class Airflow taskInherited from the taskNone unless you write it
Warehouse controlOperator parameter or connectionHook parameter or connectionManual, in your code
Best forStandard SQL stepsCustom logic mixed with SQLAd-hoc throwaway queries
Provider dependencyapache-airflow-providers-snowflakeapache-airflow-providers-snowflakesnowflake-connector-python only
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
snowflake_operator_dag.pyfrom datetime import datetime, timedelta2. SnowflakeOperator Basics
elt_dag.pyfrom datetime import datetime, timedelta3. ELT
s3_to_snowflake_dag.pyfrom datetime import datetime, timedelta5. S3ToSnowflakeOperator for Bulk Loads
queued_check.sqlSELECT7. Monitoring QUEUED State

Key takeaways

1
The Snowflake connection carries account, warehouse, database, and schema
one contract for every task.
2
SnowflakeOperator runs SQL steps; SnowflakeHook powers custom logic; ELT keeps the DAG thin.
3
A suspended warehouse queues queries
enable auto-resume and set auto-suspend deliberately.
4
S3ToSnowflakeOperator moves bulk data with retries and lineage built in.
5
Right-size warehouses by watching QUEUED time, and cap cost with resource monitors.

Common mistakes to avoid

4 patterns
×

No warehouse set in the Snowflake connection

Symptom
Queries land on a default or suspended warehouse and queue for minutes
Fix
Set warehouse, database, and schema explicitly in the connection; override per task only when needed.
×

Auto-resume disabled or a long auto-suspend on the ETL warehouse

Symptom
Every nightly run queues on a cold warehouse, or credits burn all night
Fix
Set AUTO_RESUME = TRUE and AUTO_SUSPEND to a few minutes for batch windows.
×

One oversized warehouse for every DAG

Symptom
BI queries queue behind the ELT batch and the bill climbs
Fix
Separate warehouses per workload and size each to its actual queries.
×

Zero retries on Snowflake tasks

Symptom
A transient resume blip fails the whole DAG
Fix
Set retries and a retry_delay on SnowflakeOperator so warehouse startups are absorbed.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does SnowflakeOperator do, and what connection fields does it use?
Q02SENIOR
Your Snowflake tasks are stuck in QUEUED. What do you check?
Q03SENIOR
Design a cost-controlled ELT pipeline from Airflow into Snowflake for a ...
Q01 of 03JUNIOR

What does SnowflakeOperator do, and what connection fields does it use?

ANSWER
SnowflakeOperator runs one SQL statement against a Snowflake warehouse as an Airflow task. It reads account, login, password or key, warehouse, database, and schema from the Snowflake connection, with per-operator overrides possible. It supports retries, templated SQL, and returns query results or row counts to downstream tasks.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why do my Snowflake tasks succeed but start late every night?
02
What exactly is the QUEUED state in the Grid view?
03
Should I use SnowflakeOperator or SnowflakeHook?
04
How do I know if my warehouse is too small or too big?
05
Can one Airflow DAG use multiple warehouses?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

4 min read · try the examples if you haven't

Previous
Airflow Providers
16 / 37 · Airflow
Next
Airflow dbt ELT