Stalewatch › demo

See it work

Two weeks of write activity goes in; Stalewatch learns each table's normal hours and shows the alerts it would have sent, next to what a fixed threshold would have sent.

Input

3 tables · 14 days of write activity

See the input
Output14 days of write activity
public.orders (1 of 3 tables)
hour      0     6     12    18
Wed 10-21 ........############....
Thu 10-22 ........############....
Fri 10-23 ........############....
Sat 10-24 ........................
Sun 10-25 ........................
Mon 10-26 ........############....
Tue 10-27 ........############....
Wed 10-28 ........############....
Thu 10-29 ........############....
Fri 10-30 ........############....
Sat 10-31 ........................
Sun 11-01 ........................
Mon 11-02 ........############....
Tue 11-03 ........######..##

# got rows that hour, . got none.

Show the raw query output
Output
  table_name   |    day     |          hours           
---------------+------------+--------------------------
 public.orders | 2026-10-21 | ........############....
 public.orders | 2026-10-22 | ........############....
 public.orders | 2026-10-23 | ........############....
 public.orders | 2026-10-24 | ........................
 public.orders | 2026-10-25 | ........................
 public.orders | 2026-10-26 | ........############....
 public.orders | 2026-10-27 | ........############....
 public.orders | 2026-10-28 | ........############....
 public.orders | 2026-10-29 | ........############....
 public.orders | 2026-10-30 | ........############....
 public.orders | 2026-10-31 | ........................
 public.orders | 2026-11-01 | ........................
 public.orders | 2026-11-02 | ........############....
 public.orders | 2026-11-03 | ........######..##
(14 rows)

  table_name   |    day     |          hours           
---------------+------------+--------------------------
 public.events | 2026-10-21 | ########################
 public.events | 2026-10-22 | ########################
 public.events | 2026-10-23 | ########################
 public.events | 2026-10-24 | ########################
 public.events | 2026-10-25 | ########################
 public.events | 2026-10-26 | ########################
 public.events | 2026-10-27 | ########################
 public.events | 2026-10-28 | ########################
 public.events | 2026-10-29 | ########################
 public.events | 2026-10-30 | ########################
 public.events | 2026-10-31 | ########################
 public.events | 2026-11-01 | ########################
 public.events | 2026-11-02 | ########################
 public.events | 2026-11-03 | ##################
(14 rows)

     table_name      |    day     |          hours           
---------------------+------------+--------------------------
 public.daily_rollup | 2026-10-21 | ..#.....................
 public.daily_rollup | 2026-10-22 | ..#.....................
 public.daily_rollup | 2026-10-23 | ..#.....................
 public.daily_rollup | 2026-10-24 | ..#.....................
 public.daily_rollup | 2026-10-25 | ..#.....................
 public.daily_rollup | 2026-10-26 | ..#.....................
 public.daily_rollup | 2026-10-27 | ..#.....................
 public.daily_rollup | 2026-10-28 | ..#.....................
 public.daily_rollup | 2026-10-29 | ..#.....................
 public.daily_rollup | 2026-10-30 | ..#.....................
 public.daily_rollup | 2026-10-31 | ..#.....................
 public.daily_rollup | 2026-11-01 | ..#.....................
 public.daily_rollup | 2026-11-02 | ........................
 public.daily_rollup | 2026-11-03 | ..#...............
(14 rows)

Output

2 alerts instead of 23

Across 3 tables and the last 14 days: a fixed 60-minute threshold would have sent 23 alerts, 21 of them at hours these tables are normally quiet. Learned rhythm: 2 alerts.

  1. 1 alert public.orders

    Went quiet Tue 2026-11-03 14:00–16:00. Normally gets rows: Weekdays 08:00–20:00; weekends no regular hours.

    Fixed 60-minute threshold: 10 alerts, 9 at normally quiet hours. Learned rhythm: 1 alert.

    Rhythm and quiet hours

    Normally gets rows Got rows Quiet in a normally busy hour

    000102030405060708091011121314151617181920212223
    Weekdaysbusybusybusybusybusybusybusybusybusybusybusybusy
    Weekends
    Tue 11-03rowsrowsrowsrowsrowsrowsquietquietrowsrows
  2. 1 alert public.daily_rollup

    Went quiet Mon 2026-11-02 02:00–03:00. Normally gets rows: Weekdays 02:00–03:00; weekends 02:00–03:00.

    Fixed 60-minute threshold: 13 alerts, 12 at normally quiet hours. Learned rhythm: 1 alert.

    Rhythm and quiet hours

    Normally gets rows Got rows Quiet in a normally busy hour

    000102030405060708091011121314151617181920212223
    Weekdaysbusy
    Weekendsbusy
    Mon 11-02quiet
  3. No alerts public.events

    Normally gets rows: Weekdays every hour; weekends every hour.

    Fixed 60-minute threshold: 0 alerts, 0 at normally quiet hours. Learned rhythm: 0 alerts.

    Rhythm and quiet hours

    Normally gets rows Got rows Quiet in a normally busy hour

    000102030405060708091011121314151617181920212223
    Weekdaysbusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusy
    Weekendsbusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusybusy

The Slack message for the latest alert:

public.orders got no new rows from 14:00 to 16:00 on Tue 2026-11-03. It normally gets rows on weekdays 08:00–20:00.
Raw JSON
JSONSlack Block Kit payload
{
  "text": "Stalewatch: public.orders went quiet",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*public.orders* got no new rows from 14:00 to 16:00 on Tue 2026-11-03. It normally gets rows on weekdays 08:00–20:00."
      }
    }
  ]
}

What Stalewatch did

  • Stalewatch: Learned each table's normal hours from 14 days of timestamps

    By hand: pick a threshold per table, and re-tune it when the schedule changes

  • Stalewatch: Replayed the 14 days to separate real outages from normal quiet

    By hand: a fixed threshold alerts every night and weekend, so people mute it

  • Stalewatch: Wrote the Slack message with the table, the gap and its normal hours

    By hand: write and maintain the alert yourself

Want this watching your tables?

The first 100 on the waitlist get $15/mo, locked for as long as you stay subscribed. Then it's $29/mo.

We store your email, a source tag, your consent and the signup time in Cloudflare D1, to email you about Stalewatch. Privacy

BVL privacy policy

Try it on your own tables

Run one query per table on your own Postgres and paste the output. You do not give us a database credential, and nothing you paste is stored.

Show the query to run
SQL
-- Run once per table: replace public.orders and created_at with yours.
-- Hours are read as printed, so set your time zone first.
SET TIME ZONE 'UTC';
WITH active AS (
  SELECT DISTINCT date_trunc('hour', created_at) AS hour
  FROM public.orders
  WHERE created_at >= date_trunc('day', now()) - interval '13 days'
)
SELECT 'public.orders' AS table_name,
       g::date AS day,
       string_agg(CASE WHEN a.hour IS NULL THEN '.' ELSE '#' END, '' ORDER BY g) AS hours
FROM generate_series(date_trunc('day', now()) - interval '13 days',
                     date_trunc('hour', now()), interval '1 hour') AS g
LEFT JOIN active a ON a.hour = g
GROUP BY 1, 2
ORDER BY 2;

psql's default output, psql --csv and tab-separated output all work. Paste one table's output after another, up to 20,000 characters, about 20 tables.

Settings

Limits. Nothing is sent to Slack. Rhythm is learned per hour, from at most the last 14 days you paste.