An alert that fires 300 times is mute
One failure makes people turn alerts off, and a switched-off alert is worse than one you never built.
The flapping problem
Price sits at 319.98 with a rule at 320.00. It ticks to 320.01, then 319.99, then 320.02. Each crossing is real. You get three alerts in ninety seconds and, on a busy day, three hundred.
Three hundred alerts is not a noisy alert. It is no alert, because the person stops reading them — and they will also stop reading the one that mattered.
Three mechanisms, and you need all three
Cooldown. After firing, do not fire the same rule again for N minutes. lastFiredAt from the previous lesson is exactly this. One parameter, sensible default of an hour for a price threshold.
Hysteresis. Require the price to move back past a different boundary before the rule can arm again. Fires at 320.00, re-arms below 319.00. A cooldown limits how often; hysteresis is what actually stops flapping, because it changes the condition rather than the timing.
Debounce on the observation. Require the condition to hold for two consecutive checks before firing. Costs you one interval of latency and removes single-tick noise entirely.
They solve different problems and layering all three is normal rather than excessive.
Idempotency, because the loop will run twice
Your evaluator will be run twice on the same data — a retry, an overlapping schedule, two tabs from the previous lesson. So make firing keyed:
fired_key = `${rule.id}:${observationTimestamp}`
Write it before sending, ignore duplicates. Same discipline as the ULID idempotency in any queue you will ever build, and it is the difference between "the scheduler ran twice" being invisible and being two notifications at 3am.
Group the delivery
Five rules firing in the same minute is one message with five lines, not five messages. Batch by a short window before delivery.
And put the number in it: "AAPL.US 320.15, crossed above 320.00, 14:32 UTC." Symbol, value, rule, time. A notification that says "your alert triggered" makes you open the app to find out what happened, which means it did not do its job.
Let the person see what did not fire
A rules list showing each rule's current value and distance from its threshold is worth more than the alerts. It answers "is this thing actually watching" without waiting for something to happen, and it is the surface where you notice a rule you set up wrong.
The finance behind it
Why a screen full of live opinion changes your judgement, which is the human half of alert fatigue: What does watching other people's opinions in real time do to yours?
Try it now
Replay a volatile day's data through your evaluator with cooldown and hysteresis switched off, and count the firings. Then switch them on and count again. That pair of numbers is the argument for this lesson, measured on your own rules.