Throttling doorbell notifications to the ones that matter
I rolled out face-recognition notifications on my front-door doorbell and got spammed with hundreds of pushes in a few hours. I killed the cron, audited the logic, and rewrote the dedup rule. The fix is short. The lesson is shorter.
What was happening
The notifier was firing on every face-recognition row that flipped state. "Recognized Chester" → push. "Recognized Chester again on the next frame" → another push. "Recognized Chester in the same video, frame 4" → another. Multiply by every motion event in a busy day and the phone became unusable.
The first thing I did was just stop the bleeding: comment out the cron entry with a tag I could grep for later, kill the running process. The poller and the face-recognition worker stayed running so events kept flowing into storage. Only the user-facing push was off.
#DISABLED-2026-05-10 */1 * * * * /opt/.../cass_ring_notify.py
The #DISABLED- prefix is a habit worth keeping. It's easy to grep for
("what cron entries did I disable and why?") and easy to re-enable by
deleting six characters.
What I found
The notifier had no concept of "an event." Every row that crossed a state threshold fired a notification, and a single doorbell event produces a handful of rows as the worker samples frames. There was also no concept of "this class of recognition is interesting." A recognized known person was treated as just as alert-worthy as an unknown stranger, which is the exact inverse of what I want.
Two specific bugs:
- No per-event dedup. The right granularity is one Ring video = at most one notification, regardless of how many rows that video produces.
- No status filter. Identified faces should not produce a user-visible push at all by default. Unknown faces should.
The fix
Rewrote the notifier to fire only on rows where fr_status = 'unknown'
and set a notified flag on the row after the push. The row-level flag
is the per-event dedup: each video produces exactly one row eligible
for notification (the row representing the strongest unknown match),
and once it's been notified it doesn't fire again.
def maybe_notify(rows):
for row in rows:
if row["fr_status"] != "unknown":
continue
if row.get("notified"):
continue
send_push(f"Unknown person at the front door at {row['ts']}")
row["notified"] = True
save(row)
Then I drained the backlog before re-enabling cron, marking the older
stuck rows as skipped-backlog-<date> so the new notifier wouldn't
spam them all out as a single restart artifact.
What I'd do differently
Notification systems are hard to design after the fact. The right mental model from day one is: every notification you send costs the user some attention; every notification you suppress costs them some trust. The default should be silent. Add a category, observe its real-world fire rate for a week, then decide if it's worth notifying on. If I'd done that, I would never have shipped "notify on recognized known faces" — because nobody actually wants their phone to buzz when their own front door sees them.