August 2026 · Quota watch series

How to get an alert when a data feed goes stale

A stale feed is the nastiest data failure because nothing is red. The scraper exits zero, the API answers 200, the dashboard renders, and every record on it is from last Tuesday. Uptime monitoring cannot see this by definition: the feed is up, the data is old. What you need is a freshness alarm, and there are two working ways to build one.

The manual route: cron plus a timestamp

The pattern behind every GitHub snippet on this topic is the same three lines of logic: find the newest timestamp your feed carries, compare its age against a threshold, and alert when the age wins. The timestamp can be a max(updated_at) in the database, the newest record’s date field in the feed itself, or the HTTP Last-Modified header if the source is well behaved:

newest = max(record.updated_at for record in feed) age_hours = (now_utc - newest) / 3600 if age_hours > 26: alert(f"feed stale: {age_hours:.0f}h old")

Three traps take down most homemade versions. Compare in UTC on both sides, because a timezone-naive timestamp makes the feed look eight hours stale or eight hours fresh depending on where your server lives. Set the threshold slightly past the update cadence, not at it: a daily feed deserves a 26-hour threshold, so one slow run does not page anyone. And treat an empty feed as its own alarm, not as fresh: an empty array is valid JSON with no timestamps in it, and max() of nothing either throws or, worse, gets defaulted to “now”.

The automated route: freshness as a named detection

If the feed is produced by a scraper or actor on Apify, yours or one you merely depend on, Datasource Pulse watches its freshness from the supply side with one JSON block:

{ "type": "actor", "id": "listings-feed", "label": "Listings feed · source actor", "actorId": "OWNER~ACTOR-NAME", "apifyToken": "YOUR_READ_TOKEN", "enableVolumeBaseline": true, "stalenessThresholdDays": 30 }

Three of its verdict classes are staleness in different disguises. zero_yield fires when runs keep succeeding while returning nothing, three runs in a row by default, which is exactly the “exit zero, deliver nothing” case that keeps a feed frozen. volume_drop catches the feed thinning out against its own learned baseline before it stops entirely. And actor_stale fires when the actor itself has not been updated past your threshold, the early warning that an abandoned scraper is about to become a stale feed. Each alert names the class and the numbers, and a recovery notice follows when the feed comes back.

Honest scope note: for a feed that is just an HTTP endpoint with no Apify actor behind it, Datasource Pulse’s generic_http check will catch the feed dying, changing shape, or losing auth, but the timestamp-age comparison itself stays your cron script’s job for now. The two compose well: cheap freshness math where the data lands, supply-side detection where it is produced.

Each check costs $0.02, billed per run through Apify with no subscription; one feed watched daily is about $0.61 a month. The actor output drift template for n8n emails you on anything unhealthy, and the Zapier and Make routes do the same with webhooks.

Which route should you take?

Honest answer: if you have one feed, one database, and a place to run cron, the timestamp script is the right tool and takes an afternoon including the Slack webhook. The automated route earns its keep when the feed depends on scrapers you do not control, when “successful but empty” runs are your actual failure mode, or when one stale feed is really five sources feeding one pipeline and you want one daily run with recovery notices instead of five scripts.

Either way, trip the alarm once on purpose: set the threshold below the feed’s current age, watch the alert arrive, then set it back. An alert you have never seen fire is a guess, not a monitor.

Datasource Pulse watches the data sources your product depends on: credentials, quotas, and scraper output quality. One alert when something degrades, one notice when it recovers. Freshness checks ride alongside 35+ pre-wired credential vendors, and any endpoint the list is missing is one generic_http block away.