Wazuh fix note · Wazuh 5.0 release candidate

Wazuh 5.0 monitor notifies the same burst every minute, and alerts on spread-out events

What you see: a bucket-level Alerting monitor that looks back 3 minutes (@timestamp gte now-3m) and runs every minute sends the same burst to your webhook again on each run, and it can alert on failures spread too thin to match the rule you meant. Why: the window is three times the run interval, so each event is counted by up to three runs. Each run that still sees the burst keeps the same alert open, and an action with no execution policy notifies again. What to set: on each action, action_execution_policy → per_alert with actionable_alerts: ["NEW"]. In our test that sent one notification per burst instead of three. For false alarms there is no setting that fixes it: the window decides what gets counted, and we measured the trade-off below. A shorter run interval helps a now-2m window catch spread-out failures, but not all of them (added 15 October).

Dong Nguyen, ATK New Technology · 15 October 2026 · measured on Wazuh 5.0.0-rc1 (commit aff7a0b), our test containers

5.0.0-rc1 is a release candidate, not the 5.0.0 GA release. Alerting defaults and behaviour described here may change in GA. All events were synthetic log lines pushed into the manager's engine through its local ingest socket: no real attacks, no agents.

Moving custom rules to Wazuh 5.0? Email your 4.x rule and decoder files and your Wazuh version to support@atkvn.com. We reply within 24 hours with what we find. Free. Remove hostnames, IPs and usernames first. Rather not send them? Run the free Wazuh 5.0 migration check in your browser; nothing is uploaded.


This note follows on from the Wazuh 5.0 migration check. There, under R2, we found that a 4.x rule with frequency and timeframe has no drop-in replacement on 5.0.0-rc1, and that a bucket-level Alerting monitor grouped by source.ip is the closest working path. This page covers two things that happen once that monitor runs: false alarms from a window wider than the rule you meant, and repeated notifications for one burst.

The same note as a video · 1 min 37 sEnglish captionsmeasured on Wazuh 5.0.0-rc1

How to check your monitor

All three checks are read-only. Set these first:

IDX=http://localhost:9200   # your indexer. Ours had the security plugin off: plain HTTP, no login.
CURL="curl -s"              # normal install: CURL="curl -sk -u admin:YOUR_PASSWORD" and IDX=https://...

1. Window, run interval and each action's execution policy

$CURL "$IDX/_plugins/_alerting/monitors/MONITOR_ID" | python3 -m json.tool \
  | grep -E '"name"|"from"|"interval"|"unit"|per_alert|per_execution|actionable_alerts|"NEW"|"DEDUPED"|throttle_enabled'

The stored monitor shows the window as "from": "now-3m", not as the "gte" you sent. Compare it with "interval" and "unit". Window divided by interval is roughly how many runs count the same event: 3 minutes over 1 minute is 3. Then look at each action. An action with no action_execution_policy block, or with "actionable_alerts": ["DEDUPED", "NEW"], or with per_execution, notified more than once per burst in our test.

2. Is it one alert or several?

$CURL "$IDX/_plugins/_alerting/monitors/alerts?size=200" | python3 -c '
import json,sys
for a in json.load(sys.stdin).get("alerts",[]):
    print(a["id"], a["state"], a["start_time"], a["last_notification_time"],
          (a.get("agg_alert_content") or {}).get("bucket_keys"))'

Run it once after each monitor run. In our test, one burst kept the same alert id and the same start_time across runs; only last_notification_time moved. If that is what you see, the duplicates are notifications, not alerts.

3. Are webhook notifications being refused?

$CURL "$IDX/_cluster/settings?include_defaults=true&flat_settings=true" | python3 -m json.tool | grep host_deny_list
# and in the indexer log (ours: docker logs of the indexer container)
grep "host_deny_list" /path/to/indexer.log

If your webhook points to a private address, see webhooks to private addresses below. In our first run the listener received nothing while the alert showed as active.

Why it happens

Why it alerts on spread-out events

A bucket-level monitor does not count a sliding window the way a 4.x frequency / timeframe rule does. On each run (here once a minute) it runs one search over the last 3 minutes and counts events per source.ip. Anything that fits inside 3 minutes can be counted in full by one run.

Take 8 failures from one IP, about 21.4 seconds apart, over 150 seconds. No 120-second stretch holds more than 6 of them, so a rule meant as “more than 7 in 2 minutes” should stay silent. All 8 fit in 180 seconds, so the run that lands just after the last one sees 8 and alerts. In our test it did, in 3 of 3 runs. The window, not the trigger, decides what you are counting.

A window of 2 minutes avoids that case but misses others. In earlier runs (12 October, indexer heap 1 GB), now-2m missed 8 failures spread over 90 seconds and over 110 seconds, although both have 8 inside 120 seconds: no single run saw more than 7. That monitor ran once a minute. now-3m caught both. The migration check has the full table. For what changes when a now-2m monitor runs every 30 seconds or every second, see a 2-minute window with a shorter run interval.

Why one burst is notified again and again

A short burst stays inside a 3-minute window for three runs in a row. On the first run the alert is new. On the next runs the condition is still true, so the monitor keeps the same alert and marks it deduped. When a run no longer sees the burst, the alert is completed and disappears from the alert list (alert history is off by default on 5.0.0-rc1: plugins.alerting.alert_history_enabled is false in _cluster/settings?include_defaults=true).

Each action has an execution policy that decides which of those it reacts to. This is what each one did with one 16-second burst:

action execution policynotified onnotifications for one burst
none setnew, then deduped on each following run3
per_alert, actionable_alerts: ["DEDUPED", "NEW"]new, then deduped on each following run (same as none set)3
per_alert, actionable_alerts: ["NEW"]new only1
per_executionevery run with an active alert, plus one when the alert completed4

So the repeats are not a bug in the alert itself. There was one alert per burst. The action sent it again because its policy includes deduped alerts.

The monitor we used

Created through the Alerting API (POST /_plugins/_alerting/monitors), not through the dashboard. It counts every event with a source.ip in wazuh-events-v5-* over the last 3 minutes and fires on more than 7 per IP:

{
  "type": "monitor",
  "monitor_type": "bucket_level_monitor",
  "name": "brain50-ssh-bf-now3m",
  "enabled": true,
  "schedule": { "period": { "interval": 1, "unit": "MINUTES" } },
  "inputs": [ { "search": {
    "indices": [ "wazuh-events-v5-*" ],
    "query": {
      "size": 0,
      "query": { "bool": { "filter": [ { "range": { "@timestamp": { "gte": "now-3m" } } } ] } },
      "aggregations": { "by_src": { "terms": { "field": "source.ip", "size": 100 } } }
    }
  } } ],
  "triggers": [ { "bucket_level_trigger": {
    "name": "count-gt-7",
    "severity": "1",
    "condition": {
      "buckets_path": { "_count": "_count" },
      "parent_bucket_path": "by_src",
      "script": { "source": "params._count > 7", "lang": "painless" }
    },
    "actions": []
  } } ]
}

For the false-alarm test, that is the whole monitor: no actions. For the notification test, we used the same monitor (renamed brain50-ssh-bf-now3m-dup) with four webhook actions, all pointing to one listener in the lab, all with "throttle_enabled": false. They differed only in action_execution_policy. The one that sent a single notification per burst:

{
  "name": "hook-perAlert-NEW",
  "destination_id": "brain50-hook",
  "message_template": { "source": "A=perAlert-NEW period_end={{ctx.periodEnd}} new=[{{#ctx.newAlerts}}{{bucket_keys}};{{/ctx.newAlerts}}] dedup=[{{#ctx.dedupedAlerts}}{{bucket_keys}};{{/ctx.dedupedAlerts}}] completed=[{{#ctx.completedAlerts}}{{bucket_keys}};{{/ctx.completedAlerts}}]", "lang": "mustache" },
  "throttle_enabled": false,
  "subject_template": { "source": "perAlert-NEW", "lang": "mustache" },
  "action_execution_policy": {
    "action_execution_scope": {
      "per_alert": { "actionable_alerts": [ "NEW" ] }
    }
  }
}

The message template lists which bucket keys were new, deduped or completed in each notification. That is how we read the results below.

What we measured

Wazuh 5.0.0-rc1 (commit aff7a0b), our test containers: one manager and one indexer node, indexer heap 3 GB, security plugin off. Every event was a synthetic sshd failed-login line pushed into the manager's engine through its local ingest socket (engine-ingest-http.sock, POST /events/enriched). No real attacks, no agents. IPs are documentation addresses. Times are UTC.

Test 1: false alarm on 8 failures over 150 seconds (13 October 2026)

The monitor above, no actions, running every minute. Three rounds, each with a new IP. Each round sent 8 failed logins from one IP, about 21.4 seconds apart over 150 seconds, the last one about 15 seconds before a scheduled run. Each round also sent 7 failed logins from a second IP, below the threshold, as a control. We checked the alert list 6, 66 and 126 seconds after the run.

roundIP (8 events / 150 s)runalert 6 s after the runcount in alert66 s aftercontrol IP (7 events)
1198.51.100.4116:36:01.555ACTIVE, start 16:36:01.5738goneno alert
2198.51.100.14116:42:01.555ACTIVE, start 16:42:01.5758goneno alert
3198.51.100.24116:48:01.555ACTIVE, start 16:48:01.6018goneno alert

Test 2: notifications per burst, by execution policy (14 October 2026)

The same monitor with the four actions above. One burst of 10 failed logins from 198.51.100.52 over 16.4 seconds (02:37:57.682 to 02:38:14.096), plus 7 from a control IP 203.0.113.58. After the alert had cleared, a second burst of 10 from the same IP (02:43:07.587 to 02:43:22.973). We counted what the webhook listener actually received at each run.

runalert for the burst IPno policyper alert, DEDUPED + NEWper alert, NEW onlyper execution
02:38:52ACTIVE, new alert Ac9GOKEB…1 (new)1 (new)1 (new)1 (new)
02:39:52same alert, same start time1 (deduped)1 (deduped)01 (deduped)
02:40:52same alert, same start time1 (deduped)1 (deduped)01 (deduped)
02:41:52none (burst out of the window)0001 (completed)
02:42:52none0000
02:43:52 (second burst)ACTIVE, new alert V89LOKEB…1 (new)1 (new)1 (new)1 (new)
02:44:52same alert1 (deduped)1 (deduped)01 (deduped)
02:45:52same alert1 (deduped)1 (deduped)01 (deduped)
total2 alerts6627

How to configure it

1. One notification per burst (tested)

On every action of the trigger, set:

"action_execution_policy": {
  "action_execution_scope": {
    "per_alert": { "actionable_alerts": [ "NEW" ] }
  }
}

What you give up: no repeat while the burst is still inside the window, and no message when it ends. If you also want an “ended” message, per_execution sent one when the alert completed, but it also repeated on every run while the alert was active. We set these policies when creating the monitor. We did not test editing an existing monitor, through the API or the dashboard, and we did not check which policy the dashboard's monitor editor sets by default.

This limits notifications, not alerts. The alert still shows as active in the alert list on every run while the condition holds.

2. Fewer false alarms: pick the window you mean (trade-off, measured)

No action setting changes what the monitor counts. Only the window does, and both windows we tested were wrong in some cases, with the trigger _count > 7 and a 1-minute run interval:

series from one IPhas 8 within 120 s?now-2mnow-3m
10 at onceyesalertedalerted
8 over 90 syesmissedalerted, 35 s after the last event
8 over 110 syesmissedalerted, 45 s after the last event
8 over 150 sno (6 at most)not measuredalerted (false alarm), 3 of 3 rounds
8 over 180 sno (5 at most)no alertno alert (at most 7 in any run, counted from the event times)

The 150-second row is from 13 October with a 3 GB indexer heap. The other rows are from 12 October with a 1 GB heap; see the migration check. If a missed slow attack costs you more than a short-lived false alert, now-3m is the better of the two we tested. If false alerts cost more, use now-2m and accept that series spread over 90 to 120 seconds can be missed. Running it more often than once a minute catches some of those series, not all: see the shorter run interval test below.

Not tested: other window lengths, adding a filter on event.action, and the action throttle option. We left throttle off on every action. We cannot tell you how any of these behave on 5.0.0-rc1. (A run interval shorter than one minute was tested on 15 October; see below.)

3. Webhooks to private addresses are refused by default (tested)

In our first run of test 2 the listener sat on a private Docker address. The alert was ACTIVE on both runs we checked, and the alert recorded an execution time for all four actions, yet the listener received 0 notifications. The indexer log had:

Exception sending webhook message: message creation failed with status:Host of url is denied, based on plugin setting [notification.core.http.host_deny_list]

On 5.0.0-rc1 the default of opensearch.notifications.core.http.host_deny_list (read from _cluster/settings?include_defaults=true) blocks loopback, link-local and every private range, among them 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and 100.64.0.0/10. If your webhook goes to a SOAR, an automation tool or a chat proxy inside your network, notifications fail silently: the alert looks normal and the error is only in the indexer log.

In the lab we cleared the list (PUT _cluster/settings with "opensearch.notifications.core.http.host_deny_list": []), and the second run delivered all 21 notifications in the table above. Do not do that in production. Allow only the address you need. We did not test a narrowed list.

A 2-minute window with a shorter run interval (added 15 October 2026)

A reader asked whether a now-2m monitor that runs more often than once a minute catches the 8 failures spread over 90 and 110 seconds that it missed above. We tested it on 15 October 2026 on Wazuh 5.0.0-rc1 (commit aff7a0b), our test containers. Short answer: more often helps, but it does not catch them all. Every 30 seconds, it caught the 90-second series in 7 of 12 rounds and the 110-second series in 0 of 12. Every second, it caught 5 of 5 and 4 of 5. What limits it is how late events become searchable, not how often the monitor runs.

The shortest schedule the Alerting API accepted

We sent the same monitor to POST /_plugins/_alerting/monitors with different schedules:

schedule sentresult
period 30 SECONDSaccepted (HTTP 201)
period 15 SECONDSaccepted
period 1 SECONDSaccepted
period 0 SECONDSrejected (HTTP 400): Interval is not allowed to be 0 or negative
period 500 MILLISECONDSrejected: No enum constant java.time.temporal.ChronoUnit.MILLISECONDS
period 500 or 1000 MILLISrejected: Timezone Millis is not supported expected [Seconds, Minutes, Hours, Days]
period 1000000000 NANOSrejected: Timezone Nanos is not supported expected [Seconds, Minutes, Hours, Days]
cron * * * * * (5 fields)accepted: every minute
cron */30 * * * * * (6 fields)rejected: Cron expression contains 6 parts but we expect one of [5]

“Timezone” is the plugin's own wording. The shortest schedule is "period": { "interval": 1, "unit": "SECONDS" }; a cron schedule cannot go below one minute. Changing an existing monitor from 1 MINUTES to 30 SECONDS with PUT also worked and read back as 30 SECONDS. The 1-second schedule really runs every second: a monitor left on for about 25 seconds logged 25 Executing scheduled monitor lines in the indexer log, and in the test below it ran 978 times, 0.978 to 1.023 seconds apart. The 30-second schedule ran 67 times, 29.979 to 30.022 seconds apart. We created the 15-second monitor but did not run it.

What we ran

The monitor from above with "gte": "now-2m", the same _count > 7 trigger per source.ip and no actions: first on a 30-second schedule (12 rounds), then on a 1-second schedule (5 rounds). In each round four series ran side by side, each from a new documentation IP, with the events evenly spaced: 8 failures over 90 seconds, 8 over 110 seconds, 8 over 150 seconds (never more than 6 within 120 seconds, so it must not alert), and a control of 7 over 30 seconds (below the threshold, so it must not alert). Same synthetic sshd lines through the engine ingest socket as above. All 372 events in the 30-second phase and all 155 in the 1-second phase were accepted and indexed. We did not change the manager's flush setting: no override in its internal options file and none in the container environment.

run intervalseries from one IProundsalertedmost events one run sawtime to searchable, min / max
30 s8 over 90 s127 of 128 in 7 rounds, 7 in 51.29 / 20.95 s
30 s8 over 110 s120 of 127 (6 in one round)0.95 / 20.90 s
30 s8 over 150 s120 (correct)5 or 61.39 / 20.85 s
30 scontrol: 7 over 30 s120 (correct)71.03 / 20.93 s
1 s8 over 90 s55 of 581.23 / 20.49 s
1 s8 over 110 s54 of 58 (7 in the missed round)1.01 / 20.49 s
1 s8 over 150 s50 (correct)5 or 61.01 / 20.49 s
1 scontrol: 7 over 30 s50 (correct)71.92 / 20.20 s

“Time to searchable” runs from sending an event to the first search that found it. We searched about every 0.31 seconds, so each figure can be up to about half a second high.

Why a shorter interval is not enough

The monitor selects events by @timestamp, but it can only count an event once the event is searchable. On 5.0.0-rc1, analysisd sends events to the indexer in batches on a timer, analysisd.indexer_flush_interval, about 20 seconds by default (the setting and how to change it are in Wazuh 5.0 events appear about 20 seconds late). In this test new events became searchable in batches about 20 seconds apart (median spacing 20.01 s at the 30-second schedule, 19.95 s at the 1-second one), so each event took between about 1 and 21 seconds to show up.

A run sees all 8 events only between two moments: when the last event becomes searchable, and 120 seconds after the first event, when that first event drops out of now-2m. So:

catch window = 120 s − spread of the 8 events − time the last event took to become searchable

The 30-second results depend on where the monitor's runs fall relative to the flush cycle, which is set by when the monitor is enabled. We measured one such offset per schedule; with a different one, 7 of 12 would be a different number.

Not tested: lowering the flush interval together with a short schedule. The formula above suggests a shorter flush leaves a wider window, but that is arithmetic, not a measurement; we cannot tell you how it behaves on 5.0.0-rc1. Not measured: whether the Wazuh Dashboard monitor editor offers a schedule this short (we used only the REST API), the CPU and load cost of a 1-second monitor, other offsets between schedule and flush cycle, other flush values, heavy event load, real agents, and more than one indexer node.

How we counted what each run saw

With alert history off, a run that sees 7 events leaves no record. So we added a second trigger to the same monitor, _count > 0 (severity 5, no actions), and polled the alert list without pause: its alert carries the count that run saw for each IP. Both triggers are evaluated on the same search result, so the _count > 7 trigger was unchanged. Run times are from the indexer log. We checked each run's count against one rebuilt from our own searches: no difference at 30 seconds; at 1 second, 4 runs in 3 rounds differed, each time because the monitor found a new batch a fraction of a second before our search did. The table uses the monitor's own counts.

Limits of what we measured

Everything here ran on Wazuh 5.0.0-rc1 (commit aff7a0b) in our test containers: one manager and one indexer node, security plugin off, plain HTTP. 5.0.0-rc1 is a release candidate, not GA; behaviour may change in GA. Events were synthetic sshd failed-login lines pushed through the engine ingest socket, not real attacks, and no agent was involved. We created monitors through the API, not the dashboard. Test 1 ran three rounds of one pattern (8 events over 150 seconds). Test 2 ran one burst shape (10 events over 16.4 seconds) twice, with throttle off on every action and one webhook listener; we did not test Slack, email or other channel types. A burst that stays in the window longer should be deduped on more runs, but we did not measure longer bursts. In an earlier attempt at the 150-second case (1 GB indexer heap, indexer under memory pressure), nothing fired, yet the same search run a few minutes later found all 8 events in that run's window: the last event had not been searchable in time. A monitor does not go back and re-check a window it has already run, so how fast events become searchable matters (see Wazuh 5.0 events appear about 20 seconds late). We did not measure security plugin permissions, clusters or more than one indexer node. The shorter-interval test (15 October) ran 12 rounds on a 30-second schedule and 5 on a 1-second schedule, each with one offset between the monitor's runs and the flush cycle, the default flush interval, evenly spaced series and a monitor with no actions, created through the API. We did not test a shorter flush interval with it, and did not measure the Dashboard editor, the CPU cost of a 1-second monitor, heavy load or real agents.

Free check

Check your whole manager for silent rules: Rule Doctor Lite (free, read-only, one Python file).

Lite sorts each custom rule that did not fire into DROPPED-AT-LOAD, SHADOW-CANDIDATE, NEVER-REACHES-MANAGER or NO-MATCH. We have not run Lite on 5.0.0-rc1; for rule files you plan to move to 5.0, use the Wazuh 5.0 migration check.

curl -sLO https://github.com/xuxu298/rule-doctor-lite/releases/latest/download/rule-doctor-lite.py
sudo python3 rule-doctor-lite.py