Skip to content

fix(poller): stop 15-minute watches polling every 30 minutes - #21

Merged
Aswinmcw merged 1 commit into
mainfrom
fix/poll-interval-tick-grace
Aug 28, 2026
Merged

fix(poller): stop 15-minute watches polling every 30 minutes#21
Aswinmcw merged 1 commit into
mainfrom
fix/poll-interval-tick-grace

Conversation

@Aswinmcw

Copy link
Copy Markdown
Member

Reported from the field: alerts felt like they arrived every half hour. They did.

Cause

The cron period and the shortest allowed interval are both 900s, and last_polled_at is stamped when a poll finishes — a few seconds into the tick. So the gap the due-query measures between two consecutive ticks is always 900 minus the execution time: always just under the interval.

tick 09:15:35  ->  poll completes, last_polled_at = 09:15:39
tick 09:30:35  ->  09:30:35 - 09:15:39 = 896 >= 900  is FALSE  -> skipped
tick 09:45:35  ->  1796 >= 900 -> polled

Production logs, six hours of them — cron firing reliably, polling 1 watches on every second invocation:

05:45:47  polling 1 watches
06:00:49  (skipped)
06:15:47  polling 1 watches
06:30:35  (skipped)
06:45:35  polling 1 watches
07:00:35  (skipped)
...
09:15:35  polling 1 watches

Inter-tick gaps were 886–900s throughout, so the schedule was never the problem.

migrations/0004 shows this was known: it set the column default to 840 ("14 min") specifically so a row would always be due on the next tick. But MIN_POLL_INTERVAL_SECONDS is 900 and the IntervalPicker's "15 min" preset sends 900 — so every watch created through the site got the value that breaks, while the FAQ promises "The default is every 15 minutes."

Fix

In the due-query, not by tuning magic interval values:

-  AND (last_polled_at IS NULL OR (? - last_polled_at) >= poll_interval_seconds)
+  AND (last_polled_at IS NULL OR (? - last_polled_at) >= poll_interval_seconds - ?)

POLL_TICK_GRACE_SECONDS = 60 — sized from production: 3–5s execution lag, ~14s of observed cron jitter. Far enough below one 900s tick that nothing can fire twice.

Also aligns DEFAULT_POLL_INTERVAL_SECONDS with MIN (900). A default below the enforced minimum was a trap, and the grace now covers the boundary 840 was working around. Existing 840 rows are unaffected — they poll every tick either way.

Longer intervals are unchanged beyond being at most 60s early: a 30-minute watch still needs two ticks, a 12-hour watch still needs 48.

Verification

Replayed against the live production row at the exact skipped tick:

at tick T+896s  ->  OLD query: 0 watches due    NEW query: 1 watch due

And the other direction, confirming no double-poll inside a tick:

T+5s    due=0
T+5min  due=0
T+839s  due=0
T+840s  due=1

tsc --noEmit clean; poller bundles at 49.61 KiB.

Note on rollout

This code ships in the poller worker. The poller's "Deploy default branch" trigger currently runs npx wrangler versions upload -c wrangler.poller.jsonc, which stages a version without releasing it — so merging this will not put it live. Either restore that trigger's deploy command to npx wrangler deploy -c wrangler.poller.jsonc, or run npm run deploy:poller once.

A watch whose interval is exactly 900s was checked every other cron tick,
so the site's shortest and default setting ran at 30 minutes, not 15.

The cron period and the shortest allowed interval are both 900s, and
`last_polled_at` is stamped when a poll *finishes* — a few seconds into the
tick. So the gap the due-query measures between two ticks is always 900 minus
the execution time, i.e. always just under the interval:

  tick 09:15:35 -> poll completes, last_polled_at = 09:15:39
  tick 09:30:35 -> 09:30:35 - 09:15:39 = 896 >= 900 is false -> skipped
  tick 09:45:35 -> 1796 >= 900 -> polled

Production logs show exactly this: cron firing reliably every 886-900s, but
"polling 1 watches" on only every second invocation for hours.

migrations/0004 shows the original author knew — it set the column default to
840 ("14 min") precisely so a row would always be due on the next tick. But
MIN_POLL_INTERVAL_SECONDS is 900 and the IntervalPicker's "15 min" preset
sends 900, so every watch created through the site got the value that breaks.
The FAQ meanwhile promises "The default is every 15 minutes".

Fixes it in the due-query rather than by tuning magic interval values: a watch
is due once `poll_interval_seconds - POLL_TICK_GRACE_SECONDS` has elapsed.
60s covers the 3-5s execution lag and the ~14s of observed cron jitter, and
sits far enough below one tick that nothing can be polled twice — verified at
T+5s, T+5min, T+839s (all not due) and T+840s (due).

Also aligns DEFAULT_POLL_INTERVAL_SECONDS with MIN (900). A default below the
enforced minimum was a trap, and the grace now handles the tick boundary that
840 was working around. Existing 840 rows are unaffected — they poll every
tick either way.

Verified against the production row at the exact skipped tick (T+896s):
old query returns 0 watches due, new query returns 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Aswinmcw
Aswinmcw requested review from a team and Aswin-coder as code owners August 28, 2026 09:24

@Aswincloud-Bot Aswincloud-Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: @Aswinmcw is a member of @Aswincloud/admins.

@Aswinmcw
Aswinmcw added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit 9063a25 Aug 28, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants