The video was recorded before the load tests. It does not cover them; see Under load.
How to check on your own lab
All three checks are read-only. The paths are the ones in the 5.0.0-rc1 manager container we used.
1. Is anything overriding the default?
grep -v '^#' /var/wazuh-manager/etc/wazuh-manager-internal-options.conf
If this prints nothing (or only blank lines), the compiled defaults apply. That is how the file ships in 5.0.0-rc1: it holds only comments, including “Defaults are compiled into the manager binaries; only add entries here when you need to override a specific value.” If the manager runs in a container, also look for WAZUH_INDEXER_FLUSH_INTERVAL in the container's environment. The binary contains that name; we did not test whether setting it changes the interval.
2. Does your analysisd binary have the option?
strings /var/wazuh-manager/bin/wazuh-manager-analysisd \
| grep -E 'indexer_(flush_interval|bulk_max_bytes)|INDEXER_(FLUSH_INTERVAL|BULK_MAX_BYTES)'
Our copy of the 5.0.0-rc1 binary contains these strings:
analysisd.indexer_bulk_max_bytes must be between {} and {} (got {})
analysisd.indexer_flush_interval must be between 1 and {} (got {})
analysisd.indexer_flush_interval
analysisd.indexer_bulk_max_bytes
WAZUH_INDEXER_BULK_MAX_BYTES
WAZUH_INDEXER_FLUSH_INTERVAL
This shows that the option exists, not its default. We read the default of 20 seconds from the machine code next to the option name, not from documentation; the measured cycle matches it.
3. How long does one event take to become searchable?
Write one log line that an agent already reads, then poll the indexer until the line is there. This is the same method as our test script, cut down to one event. Run it on a test install.
# /var/log/lab-auth.log must already exist and be in the agent's <localfile> list
# (log_format syslog) before you write to it.
IP=203.0.113.200 # a test IP that is not in your data; use a new one for each try
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://...
t0=$(date +%s.%N)
echo "$(date '+%b %e %H:%M:%S') $(hostname) sshd[4900]: Failed password for invalid user root from $IP port 55000 ssh2" >> /var/log/lab-auth.log
until $CURL -X POST "$IDX/wazuh-events-v5-*/_count" -H 'Content-Type: application/json' \
-d "{\"query\":{\"term\":{\"source.ip\":\"$IP\"}}}" | grep -q '"count":[1-9]'; do sleep 0.3; done
awk -v a="$t0" -v b="$(date +%s.%N)" 'BEGIN{printf "searchable after %.1f s\n", b-a}'
One try gives one number, anywhere from a couple of seconds to a little over the interval, depending on where the event lands relative to the timer. Repeat it a few times with a new IP. With the default, our longest wait was 20.4 seconds in the one-at-a-time test and 21.8 seconds in the load tests. If the loop never returns, the event was not indexed: in our run, a line written before the agent had opened the file was never indexed.
Why it happens
In 5.0.0-rc1, analysisd sends events to the indexer through its indexer connector. It does not send each event as it arrives. It collects them and sends them in a bulk request when a timer fires. The timer is analysisd.indexer_flush_interval, in seconds. It also sends early when the batch reaches the size limit analysisd.indexer_bulk_max_bytes; in our tests that happened at 500 events per second, not at 200 or below (see Under load). Until the bulk request is sent, the event cannot be found in the index.
So the delay is not constant. An event that arrives just before a flush becomes searchable soon after it (our shortest wait was 2.2 seconds); an event that arrives just after one waits a full interval. With the default, new events appear about every 20 seconds, in batches.
| internal option | environment variable in the binary | default (read from the binary) | accepted range | tested |
|---|---|---|---|---|
analysisd.indexer_flush_interval | WAZUH_INDEXER_FLUSH_INTERVAL | 20 seconds | 1 to 3600 | yes: 5, 40, default, 0 |
analysisd.indexer_bulk_max_bytes | WAZUH_INDEXER_BULK_MAX_BYTES | 8,388,608 bytes (8 MiB) | 65,536 to 104,857,600 bytes (read from the binary) | under load only: 65,536, one run each at 200 and 500 events/s |
The range 1 to 3600 is also what analysisd says when the value is out of range. We set the interval to 0 and restarted. analysisd refused to start and logged:
wazuh-manager-analysisd: ERROR: An error occurred while initializing the modules: Could not initialize Indexer Connector: analysisd.indexer_flush_interval must be between 1 and 3600 (got 0).
In the same restart, the status command reported wazuh-manager-remoted, wazuh-manager-modulesd and wazuh-manager-clusterd as not running as well. A bad value does not fall back to the default; it stops the manager from processing events. Check the status after every change.
How to change it
Add one line to the internal options file and restart the manager. As root, on the manager. Remove any earlier line for the same option first, so the file holds only one:
F=/var/wazuh-manager/etc/wazuh-manager-internal-options.conf
sed -i '/^analysisd\.indexer_flush_interval/d' $F
echo 'analysisd.indexer_flush_interval=5' >> $F
/var/wazuh-manager/bin/wazuh-manager-control restart
/var/wazuh-manager/bin/wazuh-manager-control status
After a good restart, status lists wazuh-manager-analysisd is running..., and the manager log has these two lines:
grep -E "Indexer Connector initialized|Raw Event Indexer initialized" /var/wazuh-manager/logs/wazuh-manager.log | tail -2
The file's own header says it “will not be overwritten during upgrades”. To go back to the default, delete the line and restart; in our test the cycle returned to about 20 seconds. wazuh-manager-control restart is the command we used inside the container. We did not test other ways of restarting the service.
What we measured
Wazuh 5.0.0-rc1 (commit aff7a0b), our test containers, 15 October 2026. Two paths into the manager:
- Socket: one log line per event, pushed straight into the manager's engine through its local ingest socket (
engine-ingest-http.sock). - Agent: a Wazuh 4.14.7 agent reading a log file, connected to the 5.0.0-rc1 manager over 1514/tcp.
Each run sent 12 events, one at a time. For each event we timed from sending it until a _count query on wazuh-events-v5-* first found it, polling every 0.3 seconds. After each event became searchable, the script waited 3.7 × (n mod 4) seconds before sending the next. Times are in seconds.
| path | flush interval | run | shortest wait | longest wait | cycle |
|---|---|---|---|---|---|
| socket | default (no override) | 1 | 8.8 | 20.3 | ~20 s (events became searchable 19.8 to 20.3 s apart) |
| socket | 5 | 1 | 2.2 | 6.6 | ~5 s |
| socket | 5 | 2 | 2.6 | 6.3 | ~5 s |
| socket | 5 | 3 | 2.5 | 6.3 | ~5 s |
| socket | 40 | 1 | 12.7 | 39.9 | ~40 s (39.8 to 40.3 s apart) |
| socket | override removed (default again) | 1 | 8.6 | 20.3 | ~20 s (19.7 to 20.3 s apart) |
| agent 4.14.7 | default (no override) | 1 | 2.3 | 20.4 | ~20 s (19.7 to 20.4 s apart) |
| agent 4.14.7 | 5 | 1 | 2.4 | 7.1 | ~5 s |
With 5 seconds, the gaps between events were not all 5 seconds, because our script waited different times between events. The cycle shows in where the events landed: in every 5-second run, each event became searchable at nearly the same point of the 5-second cycle, all within about 1.4 seconds of each other.
The agent path behaved like the socket path. At this load (one log line at a time, an idle agent) the 4.14.7 agent added no delay we could see on top of the flush cycle.
The shortest waits are not a typical value. They depend on where each event landed relative to the timer, and our sending rhythm locked onto the cycle. Read the table for the cycle and the longest wait, not for an average.
Under load (added 15 October 2026, after the video)
Wazuh 5.0.0-rc1, our test containers, 15 October 2026: one manager and one indexer node (OpenSearch 3.6.0, 3 GB heap, security plugin off). These tests answer three questions the first runs left open: how often the events index refreshes, what a 5-second flush costs when events arrive steadily, and whether analysisd.indexer_bulk_max_bytes sends a bulk before the timer.
How we loaded it. A generator sent sshd “Failed password” lines straight into the manager's local ingest socket, one batch every 0.1 seconds for 115 seconds, at 50, 200 or 500 events per second (it reached 50.0, 200.2 and 500.4). It did not go through agents. While it ran, a second script sent 16 marked events 1.9 seconds apart, without waiting for each to appear, and searched for them every 0.25 seconds. Over a 75-second window in each run we counted the bulk requests the indexer received, the bytes the manager container sent on its network interface, the CPU time of analysisd, and docker stats for the containers. Each load rate at each flush setting ran twice; the runs without load and the bulk-size runs ran once. Because the probe rhythm differs from the table above, compare longest waits and cycles with it, not shortest waits.
A. How often the events index refreshes
Our test event landed in the data stream wazuh-events-v5-system-activity. Its backing index has refresh_interval set to 2s, explicitly, from the index template wazuh-events-v5-system-activity-template; it is not left to the OpenSearch default. All eight wazuh-events-v5-* and all eight wazuh-findings-v5-* backing indices had 2s; wazuh-events-raw-v5, wazuh-active-responses and the three wazuh-metrics-* streams had 5s. So after a bulk arrives, an event can take up to about 2 more seconds to become searchable. Because the value is set explicitly, OpenSearch's documented search-idle pause (index.search.idle.after, 30s by default here) should not hold back refreshes on a quiet index. We did not test that.
B. What a 5-second flush costs under load
Two runs per row, shown as first / second. Bulk requests per second are what the indexer received over the window. Bytes per bulk are what the manager container sent on its network interface, divided by the number of bulks; we only recorded them from the sixth run on.
| events/s | flush | bulk requests/s | events per bulk | bytes per bulk | analysisd CPU (% of one core) | longest wait (s) | indexed / sent |
|---|---|---|---|---|---|---|---|
| 50 | 20 s (default) | 0.053 / 0.053 | 1,004 / 1,002 | not recorded | 5.2 / 5.1 | 20.7 / 21.8 | 5,755 / 5,755, both runs |
| 50 | 5 s | 0.196 / 0.198 | 252 / 252 | 302,246 / 302,770 | 6.4 / 5.2 | 6.6 / 7.0 | 5,755 / 5,755, both runs |
| 200 | 20 s (default) | 0.053 / 0.052 | 3,030* / 4,045 | not recorded / 4,790,713 | 13.3 / 14.7 | 19.4 / 21.8 | 23,020 / 23,020, both runs |
| 200 | 5 s | 0.196 / 0.197 | 1,016 / 949 | 1,211,998 / 1,232,932 | 13.1 / 11.4 | 6.8 / 6.6 | 23,020 / 23,020, both runs |
| 500 | 20 s (default) | 0.066 / 0.066 | 7,098 / 7,098 | not recorded / 8,410,769 | 26.1 / 23.7 | 14.1 / 14.5 | 57,550 / 57,550, both runs |
| 500 | 5 s | 0.197 / 0.185 | 2,577 / 2,761 | 3,075,976 / 3,104,386 | 23.4 / 22.0 | 6.3 / 6.8 | 57,550 / 57,550, both runs |
* The 75-second window cut across the 20-second cycle in this run, so it undercounts; all 23,020 events were indexed in the end. At 20 seconds the window holds only 4 or 5 bulks, so those bulk rates can be off by one bulk. Without load (one run each), the default sent 2 bulk requests in the window and the longest wait was 20.9 s; the 5-second setting sent 7, each 5,828 bytes, and the longest wait was 6.3 s.
- Nothing was lost at 5 or 20 seconds. In all 12 load runs, every event sent was indexed and every send got HTTP 200. 500 events per second is the highest rate we tried; we did not look for the limit.
- More, smaller requests, same traffic. At 5 seconds the indexer got about 0.2 bulk requests per second instead of 0.05 to 0.07, each smaller: about a quarter of the size at 50 and 200 events/s, and about 37% at 500, where the default bulks were already cut by the size limit (see the last points). The bytes the manager sent per second stayed the same: about 240 KB/s at 200 events/s and 556 to 607 KB/s at 500 events/s, at both settings.
analysisdCPU followed the event rate, not the flush interval: 5.1 to 6.4% of one core at 50 events/s, 11.4 to 14.7% at 200, 22.0 to 26.1% at 500, at both settings.- Indexer CPU: too noisy to compare. Sampled about every 2 seconds, the per-run average was 9 to 57% and single samples reached 742% (100% is one core), at both settings. We see no consistent difference between 5 and 20 seconds and, with two runs per setting, draw no conclusion. Indexer heap readings stayed between 11% and 67% of 3 GB.
- Load did not stretch the 5-second cycle: the longest wait was 6.3 to 7.0 seconds at every rate.
- At 500 events/s the default cycle got shorter. The longest wait fell to 14.1 and 14.5 seconds, and probes became searchable in batches about 14 seconds apart. Each bulk held 7,098 events in both runs, and the manager sent 8,410,769 bytes per bulk: just above 8 MiB (8,388,608 bytes), the default
analysisd.indexer_bulk_max_bytes. At 500 events per second, 7,098 events arrive in about 14.2 seconds. So the batch hit the size limit and went out before the 20-second timer. Part C tests that directly. - No empty bulks. With no load, the 5-second setting sent one bulk per cycle in which a probe was waiting (7 in the window), not one every 5 seconds.
Indexer write blocks. During the session the indexer's Security Analytics plugin logged 12 times “Events ingestion backpressure: Applying write block on 'wazuh-events-v5-*' (correlation backlog 10000/10000)”, and lifted each block 0.4 to 1.2 seconds later (11 blocks; the 12th fell between runs, so we have only its start). Around several of them it also logged that it was dropping correlation and enrichment for findings. Blocks came at both flush settings and at 50, 200 and 500 events/s, always 16 to 21 seconds after an even-numbered minute, not in step with the flush timer; we did not establish what triggers them. Our load was one event type that matches a detection rule, which may push the correlation queue harder than a mixed real stream. At 5 and 20 seconds, the manager logged no indexer warnings and every event was indexed.
C. Does analysisd.indexer_bulk_max_bytes send early? Yes (one run per rate)
We set analysisd.indexer_bulk_max_bytes=65536, the minimum, and left the flush interval at the default 20 seconds. analysisd started normally.
| events/s | bulk requests/s | events per bulk | bytes per bulk | analysisd CPU | median / longest wait (s) | indexed / sent |
|---|---|---|---|---|---|---|
| 200 | 3.58 (270 in the window) | 56 | 67,061 | 12.2% | 1.1 / 2.3 | 23,020 / 23,020 |
| 500 | 8.99 (684 in the window) | 57 | 66,969 | 25.8% | 1.5 / 2.2 | 57,438 / 57,550 |
- A size limit does send a bulk before the timer. Bulks were cut at about 64 KiB, and the indexer got about 68 times more bulk requests at 200 events/s and about 136 times more at 500 than with the defaults. The longest wait fell to 2.2 to 2.3 seconds, close to the 2-second refresh from part A.
analysisdCPU did not change. - The 500 events/s run lost 112 events. At 05:52:17 UTC the manager logged twice “Index write(s) blocked by the cluster (cluster_block_exception) - 56 document(s) in this batch (112 total) not indexed due to this expected condition. Backing off future sends.” (the first line reads “56 total”). That matches an indexer write block from 05:52:16.981 to 05:52:17.414. The final count was 112 short. With about 9 bulks a second, a block of under half a second catches some of them, and the log says those documents were not indexed. With 5- and 20-second flushes, no bulk in our 12 load runs landed in a block; that may only be chance.
- This is one run per rate, not repeated. On what we saw, we would not lower
analysisd.indexer_bulk_max_byteson 5.0.0-rc1 to cut the delay.
Limits of the load tests. The runs without load and both bulk-size runs ran once; each load rate at 5 and 20 seconds ran twice. One event type, one simulated agent, sent through the local ingest socket rather than through agents and remoted; a real mix has different document sizes and a different share of events that match rules. We stopped at 500 events per second and did not try a 1-second flush. Bytes per bulk come from the manager container's whole network interface, so they include a little other traffic. CPU from docker stats is sampled about every 2 seconds and is noisy, and the indexer ran its own periodic work. All containers ran on one machine.
Trade-offs
- A smaller interval means events become searchable sooner, and
analysisdsends bulk requests to the indexer more often. Under steady load, that means more, smaller requests. In our load tests (one manager, one indexer node, up to 500 events per second), going from 20 to 5 seconds gave about 0.2 bulk requests per second instead of 0.05 to 0.07, with the same total bytes, the sameanalysisdCPU and every event indexed. Indexer CPU was too noisy to show a difference either way. We did not measure memory or throughput beyond that, and did not test a cluster. - A smaller
analysisd.indexer_bulk_max_bytesalso sends sooner: at the minimum, 65,536 bytes, our longest wait fell to 2.2 to 2.3 seconds. It also multiplied bulk requests by about 68 to 136, and in our one run at 500 events per second, 112 events were not indexed during an indexer write block. One run per rate. - A larger interval means fewer bulk requests and a longer wait. At 40 seconds our longest wait was 39.9 seconds.
- Anything that reads the index on a schedule, such as dashboards and alerting monitors, sees an event only after its flush. We did not measure how this affects monitors.
- Keep the value inside 1 to 3600. Outside it,
analysisddoes not start.
Limits of what we measured
The one-at-a-time tests ran on Wazuh 5.0.0-rc1 (commit aff7a0b) in our test containers; the load tests ran on Wazuh 5.0.0-rc1, our test containers (images tagged 5.0.0-rc1); their notes do not record the commit. Both used one manager and one indexer node, the indexer with a 3 GB heap and the security plugin off. 5.0.0-rc1 is a release candidate, not GA; behaviour may change in GA. The default of 20 seconds and the 8 MiB default of analysisd.indexer_bulk_max_bytes were read from the binary, not from documentation. In the one-at-a-time tests our small events never came near 8 MiB. Under load we set analysisd.indexer_bulk_max_bytes only to its minimum, 65,536, once at 200 and once at 500 events per second; we did not try values in between. That the default bulks at 500 events per second were cut at 8 MiB is an inference from bulk sizes, event counts and timing, not from a log line. We did not test setting either option through its environment variable. The 40-second setting and each agent setting ran once; the 5-second socket setting ran three times. The waits come from a fixed sending rhythm that locks onto the cycle, so they are not a real-world average delay. In most runs the longest wait was a little above the interval (by 0.3 to 0.4 seconds at 20, by 1.3 to 2.1 seconds at 5); we did not separate where that extra time goes. The load tests later showed that the events indices refresh every 2 seconds and that, with bulks sent many times a second, the longest wait was 2.2 to 2.3 seconds. That fits refresh plus our polling making up most of the extra time, but we did not measure it directly. The agent ran at one event at a time; we did not try the agent's own batching settings, and we do not know how the agent path behaves under heavy load: the load tests sent events through the local socket, not through agents. During the first runs the manager was still downloading its initial vulnerability feed; the default runs before and after that gave the same cycle. We did not measure clusters or more than one indexer node. Indexer load was measured only roughly, under load: container CPU sampled about every 2 seconds and heap read three times per run. In the load tests, the runs without load and the bulk-size runs ran once; see the limits at the end of Under load.