Back to blog
FILE 0xD5·THROTTLING DOORBELL NOTIFICATIONS TO THE ONES THAT MATTER

Throttling doorbell notifications to the ones that matter

May 10, 2026 · homelab, notifications

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:

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.