How to Roll Out Freshness Monitoring in Snowflake in One Week
Freshness is the incident category with the highest cost per hour to catch late. A table that stopped loading at 3 AM and gets noticed at 11 AM by a CFO looking at a stale dashboard is a Monday nobody on the data team wants.
The good news: on Snowflake, freshness monitoring is a five-day rollout, not a quarter-long project. This is the plan.
What do you need to inventory on day 1?
You cannot monitor what you cannot list. Start with two queries against Snowflake's metadata layer.
The first query pulls every table in your production database with its last modified timestamp.
SELECT
table_catalog,
table_schema,
table_name,
last_altered,
row_count,
bytes
FROM your_prod_db.information_schema.tables
WHERE table_type = 'BASE TABLE'
AND table_schema NOT IN ('INFORMATION_SCHEMA', 'PUBLIC')
ORDER BY last_altered DESC;
The second query pulls actual load history from snowflake.account_usage.query_history filtered on INSERT, MERGE, and COPY INTO statements over the last 30 days. This is what tells you the real load cadence, not just the last-touched timestamp.
Ship the joined output to a monitoring.table_inventory table. You now have 500 to 5,000 rows depending on warehouse size. Rank by daily query count and cross-reference against your dbt exposures to identify the top 100 P0 tables. Those are what week one covers.
How do you learn each table's load cadence?
Do not set thresholds by hand. Compute them from history.
For every table in your P0 list, calculate:
- p50 load interval. Median seconds between successful writes over the last 30 days.
- p95 load interval. 95th percentile, which is your "worst normal" case.
- Weekday-vs-weekend split. Some tables load only on business days; treat them separately.
- Load count per day. Distinguishes hourly ELT (24 loads) from nightly batch (1 load).
Store these in monitoring.table_baseline with a computed_at timestamp. Recompute weekly. The baseline query itself is cheap because account_usage is materialized. Budget one XS warehouse running for 5 to 15 minutes.
The output is a per-table SLA that reflects reality. A table that loads every hour has p95 around 65 minutes. A nightly ETL has p95 around 26 hours. You never need to guess.
How do you set anomaly thresholds without alert fatigue?
Two-tier alerting, one rule.
- Warning. Time since last load > 1.5x p95.
- Critical. Time since last load > 3x p95.
That is the entire rule. Do not add manual overrides. A table with p95 of 65 minutes warns at 98 minutes and pages at 195 minutes. A nightly batch with p95 of 26 hours warns at 39 hours and pages at 78 hours. The math handles the difference between hourly and batch cadences without you tuning per table.
Two exceptions to hardcode.
- Grace period on day-of-week transitions. A table that loads Mondays through Fridays should not page over the weekend. Encode business hours per table with a simple
active_dayscolumn. - Freeze window. During deploys or maintenance, suppress alerts via a scheduled tag rather than muting the channel.
Everything else, let the math run.
How do you route alerts to the right person?
The alert must arrive with three fields: table name, cause, and owner. Without an owner, the alert becomes noise in #data-alerts and gets muted within a month.
Ownership resolution runs in this order.
- Explicit tag on the Snowflake table (
OBJECT_TAGS) with a Slack user ID or team channel. dbt manifest.jsonfieldmeta.ownerif the model has one.- Fall through to the on-call rotation for the warehouse.
Send incidents to Slack via an incoming webhook, formatted as: severity badge, table name, last load time, expected load time, downstream impact (count of dbt models and BI assets), and a link back to a runbook. One incident, one message, grouped by root cause so a stopped ingestion job produces one alert, not fifty.
What does the day-by-day schedule look like?
Five days, one engineer, no heroics.
| Day | Task | Output |
|---|---|---|
| 1 | Inventory tables and rank by criticality | monitoring.table_inventory populated, top 100 identified |
| 2 | Compute historical baselines | monitoring.table_baseline with p50, p95, weekday split |
| 3 | Deploy the freshness scan as a Snowflake task on 15-minute schedule | Task running, results in monitoring.freshness_status |
| 4 | Wire Slack routing with owner resolution | Alerts landing in owner channels, formatted |
| 5 | Tune false positives, document the runbook | Alert precision above 90%, on-call rotation locked |
By end of day 5, you have sub-hour detection on the top 100 tables, historical accuracy audited against the last 30 days of known incidents, and a config-driven system that extends to your next 500 tables without new code.
How do you handle the trickier freshness cases?
Three cases the naive approach misses.
- Views that appear fresh. A view on top of a stale base table will look fresh because it was queried recently. Monitor the base tables, not the views. Recursively resolve views to their base tables in the inventory step.
- Materialized views and clones. Snowflake dynamic tables and clones have their own refresh cadence. Include
information_schema.tableswheretable_typeisMATERIALIZED VIEWandDYNAMIC TABLEin the inventory. - Zero-row loads. An ingestion job succeeded, but zero rows were written. Freshness on
last_alteredis fine; volume monitoring catches this. If you are only doing freshness in week one, add a companion "successful load with zero rows" check for your top 20 P0 tables now.
Punt everything else to week 2. Perfection here is the enemy of shipping.
How do you measure whether the rollout worked?
Four KPIs, tracked from week 1 forward.
- Mean time to detect (MTTD). Time from an incident starting (first missed load) to the first alert. Target under 30 minutes for P0.
- Precision. Alerts that led to a real fix divided by total alerts. Target above 85% after week 2.
- Coverage. Monitored P0 tables divided by known P0 tables. Target 100% by end of week 1.
- Time to resolve (MTTR). Time from alert to resolution. Depends on the fix path, not the monitoring, but tracks trend over quarters.
If MTTD is above 60 minutes, your task interval is too long. If precision is below 70%, your p95 threshold is too tight and you need to move to 1.8x or 2x. Do not tune per table; tune the ratio globally.
The mistake to avoid
Most teams pick freshness monitoring as their first project, then scope-creep it into schema, volume, and distribution monitoring before shipping any of them. The result is a 12-week project that produces zero alerts. Ship freshness in a week with learned baselines and Slack routing. It will catch 40 to 50% of your data incidents on its own. Add volume in week two, schema in week three, distribution in week four. The compounding coverage is what protects the dashboard. A perfect design that never shipped protects nothing.
Frequently asked questions
What is a freshness SLA and how do you set one?
A freshness SLA is the maximum acceptable staleness for a table, expressed as a time. For an hourly ELT source, a reasonable SLA is 2x the load cadence: staleness above 2 hours triggers a warning, above 4 hours triggers a page. Set SLAs per criticality tier, not per table. Ten different SLA values are unmanageable; three (P0 30 min, P1 4 hr, P2 24 hr) work.
Can you monitor freshness without a dedicated tool?
Yes for the first 50 to 100 tables. A scheduled Snowflake task querying information_schema.tables and posting to Slack via system$send_snowflake_notification covers the basics. Past that, you need learned baselines (weekday vs. weekend, month-end batch loads) and ownership routing, which is where the build cost exceeds a tool's price.
How much does freshness monitoring cost on Snowflake compute?
Metadata queries are cheap. information_schema is served from cache and consumes zero warehouse credits on most operations. If you run heavier checks against query_history on a small warehouse (XS), typical overhead is under 1% of total warehouse spend, or roughly $10 to $50 per month for a mid-market team.
What is the difference between freshness and staleness?
Freshness measures whether the latest data has arrived. Staleness measures how old the latest data is. A table can be fresh (loaded 5 minutes ago) but stale (the underlying source stopped writing 6 hours ago). Freshness monitoring on load time catches ingestion failures. Business staleness requires a check on a timestamp column inside the data itself.
How do you handle tables with irregular load patterns?
Two approaches. One, tag them explicitly as event-driven in your monitoring config and skip time-based thresholds. Two, use a rate-of-change monitor instead: alert when the interval between loads exceeds 3x the p95 of the last 30 days. Do not try to fit a Gaussian to a Poisson process, it will alert constantly.
Catch broken data before dashboards do
Dalanio learns each table's normal on Snowflake, BigQuery, Redshift, and dbt, then pages your team when freshness, volume, or schema drift.
Request early access