The Storage Cost Ladder

Snowflake's cost model is elegantly simple in principle and surprisingly painful in practice. You pay for two things: compute (virtual warehouse credits, billed per second) and storage (a flat per-TB monthly rate for all data Snowflake manages). For active, frequently-queried data this is fine. The problem emerges as your data ages.

A typical Snowflake organization accumulates data faster than it retires it. Transaction logs, event streams, and raw ingestion tables that were queried constantly in their first quarter of life are barely touched twelve months later — but they're still sitting in Snowflake managed storage, accruing the same per-TB charge as your hottest production tables. The standard response is a tiering strategy: export old data as Parquet into an S3 bucket, remove it from Snowflake managed storage, and register it as an External Table so queries can still reach it. Storage cost drops dramatically.

The next step on the cost ladder is the one that causes problems. Once that S3 bucket fills up, someone eventually proposes moving the oldest partitions from S3 Standard to Glacier Deep Archive, where the per-TB cost falls to a fraction of S3 Standard pricing. A lifecycle rule is applied. The data moves. And the next morning, every External Table query that touches a tiered partition returns an error.

The trap

Snowflake's External Table engine issues synchronous HTTP Range requests to S3. Glacier objects cannot serve synchronous HTTP requests — they require an asynchronous restore that takes hours. There is no Snowflake-level mechanism to pause and retry. The query fails, and a human has to intervene.

The Warehouse Credit Drain

For Spark users, a failed job over cold data is frustrating but the cost is largely human time. For Snowflake users, the cost has a second dimension: the virtual warehouse doesn't stop running while you figure out the restore.

Consider a typical incident. A scheduled task fires at 02:00 against a Medium warehouse (4 credits/hour). The query touches a cold partition, fails immediately, and the warehouse auto-suspends after its idle timeout — let's say five minutes. The on-call engineer wakes up to the alert, submits a Glacier Standard restore request, and goes back to sleep. Restore completes four hours later. The task is manually re-triggered, the warehouse resumes, the query runs successfully, and the warehouse suspends again. During that window, you've paid for two warehouse sessions, a failed query, a full Glacier restore, the egress cost of pulling the entire file back into S3 Standard, and the minimum 90-day storage charge on the restored copy in S3 Standard — all to answer a query that might have touched 40 KB of actual Parquet data.

Standard Glacier — The full credit cost
Scheduled task fires
Warehouse resumes (billing starts)
Cold partition — query FAILS
Warehouse idles → suspends
Manual Glacier restore request
Wait 4–12 hours
Warehouse resumes again
Restored copy in S3 Standard
Query succeeds. Bill: 2x cost.

The warehouse credit waste is actually the smaller part of the bill. The more insidious cost is the restored S3 Standard copy. Glacier's minimum storage duration is 180 days for Deep Archive. A restore moves a copy of the data into S3 Standard where it must remain for that minimum period, accruing hot-storage pricing, whether you query it again or not. A single "quick historical report" can generate months of unexpected storage charges for data you never intended to bring back to hot storage.

Mounting HuskHoard as a Snowflake External Stage

HuskHoard's StreamGate is a plain HTTP server that speaks RFC 7233 Range requests — the same byte-range protocol that Snowflake uses internally when reading from S3. From Snowflake's perspective, a StreamGate endpoint is indistinguishable from a standard object store. This means you can create a Snowflake External Stage that points directly at a HuskHoard host and query archived Parquet files exactly as you would any other external data.

-- 1. Create an External Stage pointed at StreamGate CREATE OR REPLACE STAGE husk_archive_stage URL = 'http://husk-host:8080/stream/' FILE_FORMAT = (TYPE = PARQUET); -- 2. Query an archived partition directly from the stage SELECT $1:event_type::STRING AS event_type, $1:user_id::STRING AS user_id, $1:ts::TIMESTAMP AS ts FROM @husk_archive_stage/events/year=2024/month=11/ (FILE_FORMAT => (TYPE = PARQUET)) WHERE $1:event_type::STRING = 'checkout' LIMIT 1000;

When Snowflake executes this query, it first issues a HEAD request to discover the file's logical size — StreamGate answers instantly from the catalog, without touching the tape. Snowflake then issues a GET Range request for the Parquet footer (the last ~32 KB of the file), which contains the column chunk and row-group index. StreamGate consults the object_frames jump table, seeks directly to the corresponding compressed frame on the archive backend, decompresses forward, and streams the footer bytes back with a 206 Partial Content response. Snowflake uses the footer to identify which row groups satisfy the query predicate, then issues targeted range requests for only those row groups — each answered the same way, in real time, with no thaw and no intermediate copy.

The query completes in one continuous warehouse session. No idle time. No failed tasks. No restored S3 copies accumulating 90-day minimum charges in the background.

Registering an External Table over the Stage

For recurring queries against archived data, you can go one step further and register a proper External Table over the HuskHoard stage. This gives you a fully addressable, SQL-queryable table that Snowflake's query planner can reason about — including partition pruning, which we'll cover in the next section.

-- Register a partitioned External Table over the archive CREATE OR REPLACE EXTERNAL TABLE events_archive ( event_type STRING AS ($1:event_type::STRING), user_id STRING AS ($1:user_id::STRING), ts TIMESTAMP AS ($1:ts::TIMESTAMP), amount_usd FLOAT AS ($1:amount_usd::FLOAT), year STRING AS SPLIT_PART(metadata$filename, '/', 3), month STRING AS SPLIT_PART(metadata$filename, '/', 4) ) PARTITION BY (year, month) LOCATION = @husk_archive_stage/events/ FILE_FORMAT = (TYPE = PARQUET) AUTO_REFRESH = FALSE; -- Snowflake's planner prunes by partition before opening any files SELECT month, SUM(amount_usd) AS revenue FROM events_archive WHERE year = '2023' GROUP BY month ORDER BY month;
Glacier + Snowflake

Historical Query

Query fails on first cold partition, warehouse bills for idle time
Manual restore request; 4–12 hour wait; human intervention required
Full file restored to S3 Standard regardless of how little data was needed
Restored copy accrues 90-day minimum storage charge in S3 Standard
HuskHoard + Snowflake

Historical Query

StreamGate answers Range requests synchronously — query never fails or pauses
No human intervention, no restore requests, no waiting
Only the specific 16 MB compressed frames needed are fetched from cold storage
No duplicate copy created; no minimum-retention charges

The Archive Catalog as a Queryable External Table

There is a discoverability problem that precedes any query: your Snowflake users and data engineers have no way to know what has been archived, when, or what version of a file is the most recent one on tape. That information lives in HuskHoard's SQLite catalog database — invisible to Snowflake's information schema and inaccessible to any SQL query.

Running husk export --format parquet resolves this. The command projects the entire catalog table into a standard Apache Parquet file and writes it to any configured output location — your existing S3 bucket, an rclone cloud remote, or a local path. Schedule this as a Snowflake Task or a simple cron job, and register the output as an External Table in Snowflake. Your entire archive history becomes a first-class SQL object.

-- Register the HuskHoard catalog export as a Snowflake External Table CREATE OR REPLACE EXTERNAL TABLE external_metadata.husk_catalog ( file_path STRING AS ($1:file_path::STRING), version INTEGER AS ($1:version::INTEGER), tape_uuid STRING AS ($1:tape_uuid::STRING), payload_size INTEGER AS ($1:payload_size::INTEGER), compressed_size INTEGER AS ($1:compressed_size::INTEGER), blake3_hash STRING AS ($1:blake3_hash::STRING), archived_at TIMESTAMP AS ($1:archived_at::TIMESTAMP), original_mtime TIMESTAMP AS ($1:original_mtime::TIMESTAMP) ) LOCATION = @my_s3_stage/husk/catalog-export/ FILE_FORMAT = (TYPE = PARQUET) AUTO_REFRESH = FALSE; -- Which partitions were archived in the last 30 days, and how large are they? SELECT file_path, MAX(version) AS latest_version, ROUND(MAX(payload_size) / 1e9, 2) AS size_gb, ROUND( (1 - MAX(compressed_size) / NULLIF(MAX(payload_size),0)) * 100 , 1) AS compression_pct, MAX(archived_at) AS last_archived FROM external_metadata.husk_catalog WHERE archived_at > DATEADD('day', -30, CURRENT_TIMESTAMP()) GROUP BY file_path ORDER BY size_gb DESC;

Because the catalog export is a plain Parquet file in your existing S3 bucket, it participates in your normal Snowflake governance workflows. You can apply column-level security policies to control who can see file paths and tape UUIDs. You can join it against production tables to trace the lineage of data that has been externalized. You can use it to build a FinOps dashboard that shows exactly how much data is archived per day and what the corresponding on-tape storage footprint looks like — all without any connection to the HuskHoard daemon.

Partition Pruning and Frame Streaming: Two Layers of Savings

When Snowflake queries a partitioned External Table, its query planner eliminates entire files from consideration before issuing a single Range request. A query filtered to year = '2023' AND month = '11' will only open files under the /events/year=2023/month=11/ path prefix — every other partition is pruned away without any I/O. This is Snowflake's partition pruning, and it's the first layer of data reduction.

The second layer happens inside each file that does get opened. StreamGate doesn't serve Parquet files as monolithic streams. It uses the Zstd jump table in object_frames to identify the minimum set of 16 MB compressed frames that contain the bytes Snowflake requested, fetches only those frames from cold storage, and streams the decompressed result back. A Parquet file that is 8 GB compressed on tape might require only two or three 16 MB frame pulls to satisfy a predicate-filtered query that touches a handful of row groups.

Why this matters for credits

Snowflake charges credits based on how long your warehouse runs, not how much data it reads. But warehouse runtime is directly influenced by query latency. Because StreamGate answers Range requests synchronously without any thaw delay, queries over archived data run in seconds rather than failing and re-running hours later. Fewer warehouse seconds means fewer credits charged.

The combination of Snowflake partition pruning (file-level filtering) and StreamGate frame streaming (byte-level filtering) means that the vast majority of a cold archive is never touched during a typical analytical query. A compliance audit over one month of transaction data in a multi-year archive might open a dozen Parquet files and fetch a few hundred megabytes of compressed frames — not the terabytes of raw data those files represent on tape.

Practical Scenarios

Regulatory and Compliance Audits

Compliance queries are uniquely suited to HuskHoard because they are infrequent, unpredictable, and non-negotiable. A regulator or auditor doesn't accept "we'll have that data ready in twelve hours." With External Tables pointing at StreamGate, a compliance query against data archived two years ago runs exactly like a query against last week's data — same SQL, same warehouse, same turnaround time. The BLAKE3 hash recorded in the catalog at archive time gives auditors a cryptographic proof that the data returned by the query is byte-for-byte identical to what was archived, with no possibility of modification during the cold storage period.

FinOps Cost Attribution

Finance and FinOps teams frequently need to run historical cost attribution queries across years of billing and usage data. This data is a natural candidate for cold storage — it's large, it's immutable, and it's queried rarely but critically. By exporting it as partitioned Parquet, archiving it with HuskHoard, and registering it as a Snowflake External Table, you keep it fully queryable at LTO-tier economics. The catalog export even lets you build a meta-level FinOps view: how much data is archived per business unit, what the on-tape storage footprint is, and whether the archival pattern aligns with data retention policies.

ML Feature Stores over Historical Data

Training jobs that need historical features — user behavior going back eighteen months, sensor readings from a prior calendar year, transaction sequences for fraud model retraining — frequently hit cold data. Because StreamGate serves byte-range requests in real time, a Snowflake query that samples random rows across a large archived Parquet file only pulls the specific 16 MB frames that contain those rows. The training pipeline never blocks on a restore, and the archive never needs to be promoted back to hot storage just to feed a model that will sample a fraction of a percent of its rows.

0
Restore Requests

StreamGate serves every Snowflake Range request synchronously. No Glacier restore API calls, no manual intervention, no tracking restore status across dozens of partitions.

16 MB
Max Cold I/O per Request

Snowflake's partition pruning eliminates files; StreamGate's jump table eliminates everything except the exact compressed frames needed. Cold storage I/O is minimized at both layers.

$1/TB
Archive Storage Cost

LTO tape or Deep Archive pricing, with no mandatory minimum-retention restore copies and no data duplication back to S3 Standard just to run a query.

Snowflake's promise to its customers is that all your data lives in one place and is always queryable. The reality of storage economics means that promise breaks the moment data ages into cold storage — unless something bridges the gap. HuskHoard's StreamGate is that bridge: a transparent HTTP layer that makes deep archive look, to Snowflake, exactly like any other object store endpoint. Your cold data stays cold, your credits go toward actual computation, and your External Tables stop failing at partition boundaries.