About hourwatch

The origin

I run a YouTube channel with about 10,000 subscribers. Every time I publish a video, I refresh YouTube Studio for hours. Thumb on F5, watching the view count tick up. I'd call it "checking analytics," but it was just anxiety with a UI.

Here's the thing: nothing I do in those first few hours changes the numbers. The refresh does nothing. So I stopped doing the useless part myself and built something to do it for me. hourwatch watches the view count so I don't have to, and only bothers me when there's actually something worth knowing.

The pitch

Publish your video, put your phone away. We'll tell you what matters.

It catches a new upload the moment it goes public, checks the view count every hour, and once there's enough history, compares it against your channel's own average. You get a notification when something's off, not every hour on the hour. The 6-hour window is just the default. You're not locked into it.

How it works

TanStack Start on Cloudflare Workers, D1 with Drizzle, Better Auth for Google OAuth, Web Push for notifications.

A few decisions mattered more than the rest:

Upload detection runs on WebSub/PubSubHubbub instead of polling. YouTube's hub POSTs to a webhook the second a video goes public, so detection is close to instant instead of up to an hour stale.

I track views, not CTR. The Analytics API has a ~48-hour delay, which makes any CTR-based "catch it early" feature pointless. By the time the data shows up, the window's gone. The Data API's videos.list statistics update in near real time and cost 1 quota unit per call, so checking a lot of videos hourly is cheap.

All the scheduling happens server-side. Service workers don't reliably stay alive for 6 hours in the background, especially on iOS, so I didn't even try. Hourly snapshots run as a Cloudflare Cron Trigger instead.

Problems I actually hit

WebSub's handshake fails silently if you get it wrong. A channel that quietly stops getting notified a week in is exactly the kind of bug that kills trust before you even know it's happening. I added a daily renewal cron that re-subscribes before the ~5-day lease runs out.

Baseline comparisons need history to mean anything. So notifications degrade on purpose: raw numbers for new channels, baseline comparisons once you've got roughly 3 videos of data.

D1 doesn't support interactive transactions, so everything runs through db.batch(). The hourly cron has to process every video in its window in one run, and handle late webhook deliveries by skipping the hours it missed instead of faking data to fill the gap.

What I learned

The engineering was fun (WebSub, web push, cron on the edge), but that's not really the point. The point is I built something that made a habit I was a little embarrassed by just... not necessary anymore. It's still rough, the pacing math still needs work, but it works. I publish a video now and go do something else.

The core of it, the pacing check at hour $t$ against the channel's prior average $\bar{v}$:

$$\text{pacing} = \frac{v_t - \bar{v}}{\bar{v}} \times 100\%$$

Built With

Share this project:

Updates

Submission history