Most Suricata tuning guides give you a list of numbers to paste into suricata.yaml. That does not help when your sensor still drops packets, because every network and every server is different.
In this guide, you will learn to build a safe test network on one Linux server, send the same traffic every time, and read the Suricata counters after each change. If the drop number goes down, you keep the change. If it does not, you undo it.
That is the safest way to do Suricata 8 packet drop performance tuning on your own hardware.
What You Need
For Suricata 8 packet drop performance tuning, make sure you have these things ready:
- A Linux server with root or sudo access. This guide uses Ubuntu 24.04 LTS.
- At least 4 CPU cores and 8 GB RAM. The examples assume 8 cores. If you have fewer, adjust the CPU numbers.
- Suricata 8.0.x LTS.
- Basic Linux command line skills.
This guide is for passive monitoring (IDS mode), where Suricata only watches a copy of the traffic. If you run Suricata inline as an IPS, some AF_PACKET settings must be different. You must check the note in Round 1.
Why Suricata Packet Loss is a Counter Problem
A packet can be lost in several places. Some are before Suricata sees it, and some are inside Suricata. Knowing which place is losing packets saves time.
| Where |
What happens |
How to see it |
| NIC hardware |
The NIC ring is full, so the card drops the packet |
ethtool -S IFACE (look for missed, no_buffer, fifo) |
| Kernel backlog |
The kernel queue is full before the packet reaches the capture socket |
/proc/net/softnet_stat (second column) |
| AF_PACKET ring |
Suricata reads too slowly, so the ring fills up |
capture.kernel_drops in Suricata stats |
| Suricata memory limits |
Flow or stream memory (memcap) is full |
tcp.ssn_memcap_drop, tcp.segment_memcap_drop |
| Stream engine |
Data is missing inside a TCP stream |
tcp.reassembly_gap |
The main sign of packet loss in Suricata is capture.kernel_drops. Ideally it stays at 0. It should never go above 1% of capture.kernel_packets.
Also, your NIC can drop packets, and Suricata never counts those. So check ethtool as well.
In AF_PACKET mode, kernel_drops counts packets that the kernel threw away before they reached Suricata.
The tcp.reassembly_gap counter should also be 0. It goes up when packets are lost. It also goes up when checksums are bad or when Suricata runs out of memory for streams.
We will use this simple formula in every test:
drop percent = kernel_drops / kernel_packets x 100
A tuning change only counts if you can compare it with a before number. Here is the plan:
- Build a lab that creates the same traffic every time.
- Start with a weak setup on purpose, so it drops packets.
- Change one thing.
- Run the same test again and compare the counters.
Never change two settings in the same round. If you do, you will not know which one helped.
Step 1: Install Suricata 8 and the Test Tools
First, update the server and install the tools we need with the commands below:
sudo apt updatesudo DEBIAN_FRONTEND=noninteractive apt install -y software-properties-common \ iperf3 jq ethtool sysstat
Add the official OISF stable repository and install Suricata. The OISF keeps a PPA named suricata-stable that always has the latest stable release:
sudo add-apt-repository -y ppa:oisf/suricata-stablesudo apt updatesudo apt install suricata -y
Check the version and the build features with the commands below:
suricata -Vsuricata --build-info | grep -i -E "hyperscan|af_packet"
In the build info, look for a Hyperscan line that says yes. If Hyperscan says no, you can still follow this guide, but skip the Hyperscan part of Round 5 and use mpm-algo: ac-ks instead.
The package starts Suricata as a service. Stop it, because our test script will start Suricata by itself with a different log folder each time:
sudo systemctl disable --now suricata
Make a safe copy of the original config. You will use it to undo mistakes:
sudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.orig
Step 2: Build the Traffic Lab
We create a virtual network cable, called a veth pair. One end stays on the host, and Suricata watches it. The other end goes inside a separate network space called gen. This is where we run the traffic generator:
sudo ip netns add gensudo ip link add lab0 type veth peer name gen0sudo ip link set gen0 netns gen sudo ip addr add 10.10.10.1/24 dev lab0sudo ip link set lab0 up sudo ip netns exec gen ip addr add 10.10.10.2/24 dev gen0sudo ip netns exec gen ip link set gen0 upsudo ip netns exec gen ip link set lo up
Then, use the command below to test the link:
sudo ip netns exec gen ping -c 2 10.10.10.1
If the ping fails and you use ufw, you must allow the lab interface:
sudo ufw allow in on lab0
Now start two iperf3 servers on the host. We set them to CPU 0 so they stay away from the Suricata worker CPUs:
sudo taskset -c 0 iperf3 -s -D -p 5201 -B 10.10.10.1sudo taskset -c 0 iperf3 -s -D -p 5202 -B 10.10.10.1
Why two servers? One iperf3 server can run only one test at a time. We want two tests at the same time. The first is a TCP test with many flows and large packets. The second is a UDP test with small packets and a high packet rate.
Suricata will watch lab0. AF_PACKET sees traffic in both directions on an interface. So Suricata will see the client packets and the server replies.
Step 3: Prepare Rules and the Baseline Config
In this step, you can download the rules and set up a weak starting config on purpose. Rules make Suricata use CPU, so they give the lab real work to do. The weak config will drop packets, which gives you a baseline number to compare with in every later round.
Download the rules
Rules are what make Suricata use CPU. Download the free Emerging Threats Open ruleset with the built-in updater:
sudo suricata-updatels -lh /var/lib/suricata/rules/suricata.rules
Edit suricata.yaml
Now use your desired text editor to open the config file:
sudo nano /etc/suricata/suricata.yaml
First, find the HOME_NET line and set it to the lab network:
1vars:2 address-groups:3 HOME_NET: "[10.10.10.0/24]"
Next, find the af-packet: section. Replace the first interface entry, the - interface: eth0 block, with this weak baseline. Leave the - interface: default block below it as it is:
1af-packet:2 - interface: lab03 threads: 14 cluster-id: 995 cluster-type: cluster_flow6 defrag: yes7 tpacket-v3: no8 ring-size: 2048
threads: 1 gives you only one worker, so one CPU core does all the work.
tpacket-v3: no uses the older AF_PACKET version 2.
ring-size: 2048 is a small buffer. It holds only 2048 packets per thread.
Then, find the stats log output and turn on per-thread numbers. This helps you see which worker is in trouble:
1 - stats:2 enabled: yes3 filename: stats.log4 append: yes5 totals: yes6 threads: yes
Also, you should find the pattern matcher, detect settings, and set the weak baseline. You can use grep to find the line numbers:
sudo grep -n -E "^mpm-algo|^spm-algo|^ profile:|^runmode" /etc/suricata/suricata.yaml
Set these values with the lines that grep shows you:
1mpm-algo: ac2spm-algo: auto3 4detect:5 profile: medium
Your file may already show medium for the profile and auto for the algorithm. The only change here is mpm-algo: ac, which gives you a slower matcher to start from. Save the file and test it:
sudo suricata -T -c /etc/suricata/suricata.yaml -v
You should see a message that the configuration was loaded successfully. You must fix any YAML errors before you continue. Indentation errors are the most common problem. Save a copy of this baseline:
sudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round0
You can always print the settings Suricata really loaded. You can use this check for AF_PACKET:
suricata -c /etc/suricata/suricata.yaml --dump-config | grep af-packet
Step 4: Create the Test Script
At this point, you can create one script that runs a full test:
- Stops any old Suricata and starts a fresh one with its own log folder.
- Waits until Suricata is ready.
- Sends TCP and UDP traffic from the
gen namespace.
- Saves per-thread CPU use in the middle of the test.
- Stops Suricata, reads the final stats, and adds one line to a CSV file.
Use the commands below to create the folder and file, and paste the following script to the file:
sudo mkdir -p /opt/suri-lab /var/log/suricata-labsudo nano /opt/suri-lab/run-test.sh
set -uo pipefail LABEL="${1:?Usage: run-test.sh <label>}"DUR="${DUR:-30}"TCP_RATE="${TCP_RATE:-100M}"UDP_RATE="${UDP_RATE:-50M}"CLIENT_CPUS="${CLIENT_CPUS:-1}" BASE=/var/log/suricata-labRUN="$BASE/$(date +%H%M%S)-${LABEL// /_}"CSV="$BASE/results.csv" [ "$EUID" -eq 0 ] || { echo "Run this script as root."; exit 1; }mkdir -p "$RUN"[ -f "$CSV" ] || echo "time,label,kernel_packets,kernel_drops,drop_pct,reassembly_gap,ssn_memcap_drop,segment_memcap_drop,flow_emerg,wrong_thread,softnet_drops" > "$CSV" softnet_drops() { local s=0 a b while read -r a b _; do s=$((s + 16#$b)); done < /proc/net/softnet_stat echo "$s"} pkill -f "suricata -c" 2>/dev/nullsleep 3SN_BEFORE=$(softnet_drops) suricata -c /etc/suricata/suricata.yaml --dump-config 2>/dev/null \ | grep -E "af-packet|mpm-algo|spm-algo|detect.profile|bypass|memcap|cpu-affinity|max-pending" \ > "$RUN/config-snapshot.txt" suricata -c /etc/suricata/suricata.yaml --af-packet \ -l "$RUN" --pidfile "$RUN/suricata.pid" -D READY=0for i in $(seq 1 150); do if grep -q "Engine started" "$RUN/suricata.log" 2>/dev/null; then READY=1; break; fi sleep 2done[ "$READY" -eq 1 ] || { echo "Suricata did not start. Check $RUN/suricata.log"; exit 1; }PID=$(cat "$RUN/suricata.pid")echo "Suricata is ready (PID $PID). Sending traffic for ${DUR}s..." ip netns exec gen taskset -c "$CLIENT_CPUS" iperf3 -c 10.10.10.1 -p 5201 \ -P 16 -t "$DUR" -b "$TCP_RATE" > "$RUN/iperf-tcp.txt" 2>&1 &ip netns exec gen taskset -c "$CLIENT_CPUS" iperf3 -c 10.10.10.1 -p 5202 \ -u -P 4 -l 200 -t "$DUR" -b "$UDP_RATE" > "$RUN/iperf-udp.txt" 2>&1 & sleep $((DUR / 2))pidstat -t -p "$PID" 5 1 2>/dev/null | grep -E "TGID|W#|Average" > "$RUN/threads-cpu.txt" waitsleep 12 kill "$PID" 2>/dev/nullfor i in $(seq 1 60); do kill -0 "$PID" 2>/dev/null || break; sleep 1; done SN_AFTER=$(softnet_drops)LAST=$(grep '"event_type":"stats"' "$RUN/eve.json" | tail -n 1)[ -n "$LAST" ] || { echo "No stats event found in $RUN/eve.json"; exit 1; } read -r KP KD GAP SSN SEG EMERG WT < <(echo "$LAST" | jq -r '[ (.stats.capture.kernel_packets // 0), (.stats.capture.kernel_drops // 0), (.stats.tcp.reassembly_gap // 0), (.stats.tcp.ssn_memcap_drop // 0), (.stats.tcp.segment_memcap_drop // 0), (.stats.flow.emerg_mode_entered // 0), (.stats.tcp.pkt_on_wrong_thread // 0)] | @tsv') PCT=$(awk -v k="$KP" -v d="$KD" 'BEGIN{ if (k>0) printf "%.2f", d*100/k; else print "0.00" }')SN=$((SN_AFTER - SN_BEFORE)) echo "$(date +%T),$LABEL,$KP,$KD,$PCT,$GAP,$SSN,$SEG,$EMERG,$WT,$SN" >> "$CSV" echoecho "===== RESULT: $LABEL ====="echo "kernel_packets : $KP"echo "kernel_drops : $KD ($PCT %)"echo "tcp.reassembly_gap : $GAP"echo "ssn_memcap_drop : $SSN"echo "segment_memcap_drop : $SEG"echo "flow emergency mode : $EMERG"echo "pkt_on_wrong_thread : $WT"echo "kernel backlog drops: $SN"echo "Details saved in : $RUN"
Once you are done, save and close the file. Make it executable:
sudo chmod +x /opt/suri-lab/run-test.sh
Step 5: Run the Baseline
Now you can run the first test with the weak config. This gives you the "before" numbers. You should see packet drops here, so you have something to fix. If you see no drops, you must raise the traffic rate until you do.
sudo /opt/suri-lab/run-test.sh round0-baseline
If kernel_drops is 0, your test traffic is too light. Raise the rates and run the baseline again:
sudo TCP_RATE=300M UDP_RATE=150M /opt/suri-lab/run-test.sh round0-baseline-heavy
Run each test twice. If the two results are very different, your server has other load, and you should find it before you trust any result. Look at the CPU use per thread:
cat /var/log/suricata-lab/*round0*/threads-cpu.txt
With one worker, you should see one W#01-lab0 thread near 100% CPU. That is the bottleneck we are going to fix.
Step 6: Tune Step by Step and Check the Counters
In this step, you must change one setting at a time and run the same test again. After each round, you must compare the new numbers with the old ones.
If the drops go down, you can keep the change. If they do not, you can undo it. This way, you always know which change helped. Use this routine for every round:
sudo nano /etc/suricata/suricata.yaml sudo suricata -T -c /etc/suricata/suricata.yaml -v sudo /opt/suri-lab/run-test.sh roundN-name tail -n 5 /var/log/suricata-lab/results.csv
If a round makes things worse, restore the last file. For example:
sudo cp /etc/suricata/suricata.yaml.round2 /etc/suricata/suricata.yaml
Round 1: AF_PACKET version (TPACKET v3)
AF_PACKET is the Linux capture method. It has version 2 and version 3 ring styles. It is recommended to use v3 for IDS and network monitoring, and v2 for IPS.
Suricata 8 uses v3 by default in non-inline modes. In inline modes, v3 stays off. The default block size is also bigger now. It grew from 32 KiB to 128 KiB. So a fresh Suricata 8 config probably uses v3 already. This round matters most if you moved an old config over from Suricata 7.
Edit the af-packet block. Change only the version line:
Once you are done, test and run with the commands below:
sudo suricata -T -c /etc/suricata/suricata.yaml -vsudo /opt/suri-lab/run-test.sh round1-tpacket-v3sudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round1
Note: Running Suricata inline as an IPS? Skip this round. Inline mode should stay on TPACKET v2. Your packet copy settings (copy-mode and copy-iface) must also match your IPS setup. Follow the full steps in our Suricata 8 inline IPS tutorial. Use this lab only to measure the counters, not to change the AF_PACKET version.
Round 2: Thread count
With one worker, only one core does all the work. More workers share the work across more cores. With cluster_flow, all packets of one connection go to the same worker. So the work is split by connection.
On an 8-core server, we use CPU 0 for the system and the iperf3 servers. We use CPU 1 for the traffic client. That leaves CPUs 2 to 7, which gives Suricata 6 cores for workers.
When you are done, test and run with the following commands:
sudo suricata -T -c /etc/suricata/suricata.yaml -vsudo /opt/suri-lab/run-test.sh round2-threads6sudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round2
Now check the per-thread CPU with the following command:
cat /var/log/suricata-lab/*round2*/threads-cpu.txt
- Good sign: Several
W# threads share the load.
- Warning sign: One thread sits at 100% while the others are idle. One big connection is stuck on one worker. More threads cannot split a single connection.
- Fix: Use stream bypass in Round 6.
Also, you can look at drops per thread:
grep kernel_drops /var/log/suricata-lab/*round2*/stats.log | tail -n 8
Do not use more worker threads than the CPU cores you can give to Suricata. If two threads share one core, they slow each other down.
Round 3: Cluster mode
The cluster mode tells the kernel how to split packets between your worker threads. The sample config lists these options:
cluster_flow: all packets of a flow go to the same socket.
cluster_cpu: all packets handled by one CPU in the kernel go to the same socket.
cluster_qm: all packets from one NIC RSS queue go to the same socket, which needs Linux 3.14 or newer.
For most servers, cluster_flow is the right choice and default option. Use this table for real hardware:
| Your NIC |
RSS queues |
Cluster type |
| Basic NIC, no symmetric hash |
Set to 1 |
cluster_flow |
| High-end NIC with symmetric RSS set up |
Same as worker threads |
cluster_qm |
To see and change RSS queues on a real NIC, you can use the commands below with your interface name:
ethtool -l ens3sudo ethtool -L ens3 combined 1
Read your driver notes before you change queues. On some drivers, a wrong change can cause problems.
In this lab, the veth link has only one queue, so cluster_qm does not apply. Keep cluster_flow.
You can still check the result. Watch tcp.pkt_on_wrong_thread in your results. It shows load balancing problems. With cluster_flow, it should stay at 0 or very low. Set it clearly in the config:
cluster-type: cluster_flow
When you are done, test and run with the following commands:
sudo /opt/suri-lab/run-test.sh round3-cluster-flowsudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round3
Note: every AF_PACKET interface needs its own cluster-id. Never reuse the same ID on two different interfaces.
Round 4: Ring size
The ring is a waiting room for packets. It sits between the kernel and Suricata. Each thread has its own ring, and ring-size sets how many packets it can hold. If a ring fills up, the drop counters go up. The memory use is set at start, and the formula is:
threads x ring-size x (default-packet-size + ~750 bytes)
With default-packet-size at 1514, one packet slot costs about 2264 bytes. Here are two examples:
6 threads x 10,000 x 2264 bytes = about 136 MB
6 threads x 100,000 x 2264 bytes = about 1.36 GB
In Suricata 8 packet drop performance tuning, ring size is usually the first setting people raise. But a bigger ring only helps with short bursts. It does not make Suricata faster. If Suricata is always too slow, a big ring just fills up later. Test it in small steps:
sudo suricata -T -c /etc/suricata/suricata.yaml -vsudo /opt/suri-lab/run-test.sh round4-ring10k
Then, you can try a bigger value:
sudo /opt/suri-lab/run-test.sh round4-ring50k
How to read the result:
- Drops fall a lot, and CPU is not full: your problem was bursts. Keep the ring.
- Drops barely change, and the worker threads are near 100%: the ring is not the problem. Go to the CPU rounds.
- Memory is getting tight: go back to the smaller ring.
If you leave out ring-size, Suricata calculates it from max-pending-packets and the thread count. Set it yourself, which is more predictable.
With TPACKET v3, you also have block-size. It must be a power of 2 and a multiple of the page size, usually 4096. The official high-performance example uses ring-size: 100000 with block-size: 1048576. Change the block size only if your ring test shows you need it.
For busy systems, there is one more option, which is use-emergency-flush: yes. It helps the ring recover after a drop phase. The cost is that some packets will not be inspected. Use it only if you accept that.
Keep the best ring value and save it:
sudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round4
Round 5: Hyperscan and the detection profile
If all workers are busy and drops continue, look at detection cost next. It is a common cause of CPU saturation in Suricata 8 packet drop performance tuning work.
The pattern matcher (mpm-algo) searches packets for rule patterns. Hyperscan is the best choice on supported platforms. Without Hyperscan, ac-ks is better than the default ac.
First, you must confirm your build supports it, which you did in Step 1:
suricata --build-info | grep -i hyperscan
Edit the top-level settings and the detect profile:
1mpm-algo: hs2spm-algo: hs3 4 5detect:6 profile: high
The profile sets how Suricata groups rules. With Hyperscan, use high or a bigger custom group size. Do not use detect.sgh-mpm-context: full, because it makes startup much slower. For a custom size, you can use this format:
1detect:2 profile: custom3 custom-values:4 toclient-groups: 1005 toserver-groups: 100
Do this in two small tests so you know what helped. First, only mpm-algo: hs, then profile: high:
sudo suricata -T -c /etc/suricata/suricata.yaml -vsudo /opt/suri-lab/run-test.sh round5a-hyperscansudo /opt/suri-lab/run-test.sh round5b-profile-highsudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round5
Expect longer startup time and more RAM with high. If the start takes too long or memory is tight, go back to medium.
If there is no Hyperscan on your build, use mpm-algo: ac-ks and test that instead.
Round 6: Stream bypass
Some flows are huge and boring, like a big file download. Suricata does not need to inspect all of it. The stream.bypass option stops inspection once a flow reaches a set depth. The inspection is skipped when stream.reassembly.depth of 1 MB.
You can find the stream: block and set:
1stream:2 bypass: yes3 reassembly:4 depth: 1 MiB
Keep any other lines already in the block, such as memcap.
Also, there is a separate setting for encrypted traffic. In Suricata 8, it no longer depends on stream.bypass. You control TLS and SSH on their own. The options are bypass, track-only, or full. For TLS, find encryption-handling under app-layer.protocols.tls:
1app-layer:2 protocols:3 tls:4 enabled: yes5 encryption-handling: bypass
sudo suricata -T -c /etc/suricata/suricata.yaml -vsudo /opt/suri-lab/run-test.sh round6-bypass
Note: Once a flow is bypassed, rules cannot match its later data. An attacker could hide something deep inside a long connection. Many teams accept this for bulk traffic and keep full inspection for everything else. It is your risk, so test first and then decide.
Round 7: Memcaps
A memcap is a memory limit. When a limit is full, Suricata drops state or data, and that creates gaps even if the capture itself has no drops. Look for signs first, and do not raise limits without a reason:
grep -i -E "memcap|emerg" /var/log/suricata-lab/*/stats.log | tail -n 20
We already log three counters:
tcp.ssn_memcap_drop: the stream engine ran out of memory for new sessions.
tcp.segment_memcap_drop: the reassembly engine ran out of memory for segments.
flow.emerg_mode_entered: the flow table got too full, so Suricata went into emergency mode.
If you see memcap values in the stats, you can raise the memcap in the config. But this uses more RAM, so plan for it.
If any of these counters is above 0, raise only the matching limit. First, check the real names and current values in your file:
sudo grep -n -E "memcap" /etc/suricata/suricata.yaml
For example, on an 8 GB lab server:
1flow:2 memcap: 256 MiB3 4stream:5 memcap: 256 MiB6 reassembly:7 memcap: 512 MiB
sudo /opt/suri-lab/run-test.sh round7-memcaps
If all memcap counters were 0 in earlier rounds, skip this round. Bigger limits will not fix a problem you do not have.
Also, tcp.reassembly_gap can stay above 0 after a memcap fix. Bad checksums and packet loss before Suricata can cause it too.
Round 8: NIC offloads and NIC ring
Network cards and drivers can merge small packets into larger super-packets (LRO and GRO). This breaks the dsize keyword and TCP state tracking, so these features should be off. For AF_PACKET, checksum offload (rx and tx) can stay on.
Check the current state of the lab link with the command below:
ethtool -k lab0 | grep -E "generic-receive-offload|large-receive-offload|tcp-segmentation-offload|generic-segmentation-offload"
Turn the merge features off on both ends of the lab cable:
sudo ethtool -K lab0 gro off lro offsudo ip netns exec gen ethtool -K gen0 gro off lro off
In a virtual link, some features may show as fixed or Cannot change. That is fine. Skip them. Run the test:
sudo /opt/suri-lab/run-test.sh round8-offloads
On a real server NIC, also check the NIC's own ring and error counters. In the ethtool -S, fields like rx_missed_errors and rx_no_buffer_count are shown as signs of NIC drops:
ethtool -g ens3ethtool -S ens3 | grep -i -E "drop|miss|fifo|no_buffer|over"
If ethtool -g shows that the current RX ring is lower than the maximum, you can raise it. For example:
sudo ethtool -G ens3 rx 4096
Use a value that your own ethtool -g output says is allowed. Offload and ring changes are lost after a reboot. To keep them, you can create a small service:
sudo nano /etc/systemd/system/suricata-nic-tune.service
[Unit]Description=Capture NIC tuning for SuricataAfter=network.targetBefore=suricata.service [Service]Type=oneshotRemainAfterExit=yesExecStart=-/usr/sbin/ethtool -K ens3 gro off lro offExecStart=-/usr/sbin/ethtool -G ens3 rx 4096 [Install]WantedBy=multi-user.target
sudo systemctl daemon-reloadsudo systemctl enable suricata-nic-tune.service
The - before each command tells systemd to continue even if the NIC says a feature cannot be changed.
Round 9: CPU affinity
Without affinity, Linux can move Suricata threads between cores. That hurts the CPU cache. With affinity, each worker stays on its own core.
In Suricata 8, the threading.cpu-affinity settings use a dictionary format instead of the old list format. Find the threading: block and use this layout for the 8-core lab:
1threading:2 set-cpu-affinity: yes3 cpu-affinity:4 management-cpu-set:5 cpu: [ 0 ]6 worker-cpu-set:7 cpu: [ "2-7" ]8 mode: "exclusive"9 prio:10 default: "high"
On a server with more than one CPU socket (NUMA), you must keep the workers on the same NUMA node as the capture NIC. Also, avoid CPU 0 for workers where you can. Check your layout and the NIC's node:
lscpu | grep -i numacat /sys/class/net/ens3/device/numa_node
For a high-speed setup, irqbalance should be off during tuning:
sudo systemctl stop irqbalance
Once you are done, run the final test with the commands below:
sudo suricata -T -c /etc/suricata/suricata.yaml -vsudo /opt/suri-lab/run-test.sh round9-affinitysudo cp /etc/suricata/suricata.yaml /etc/suricata/suricata.yaml.round9
Check that the workers stay on their cores. In threads-cpu.txt, the CPU column shows which core each W# thread uses:
cat /var/log/suricata-lab/*round9*/threads-cpu.txt
Each worker should stay on one core from the worker-cpu-set range, 2 to 7.
Step 7: Read the Results
In this step, you must look at the numbers from all your rounds. You can put them in one table and compare each round with the baseline. Then, you use a simple symptom table to see what each counter means and what to fix next. Open the CSV file:
column -s, -t /var/log/suricata-lab/results.csv
Fill a table like this one with your own numbers. We do not show sample numbers here on purpose. Your hardware and your traffic will give different results, and only your own numbers count.
| Round |
Change |
kernel_packets |
kernel_drops |
Drop % |
reassembly_gap |
Keep it? |
| 0 |
Baseline |
|
|
|
|
|
| 1 |
TPACKET v3 |
|
|
|
|
|
| 2 |
6 threads |
|
|
|
|
|
| 3 |
cluster_flow |
|
|
|
|
|
| 4 |
ring-size |
|
|
|
|
|
| 5 |
Hyperscan + high profile |
|
|
|
|
|
| 6 |
Stream bypass |
|
|
|
|
|
| 7 |
Memcaps |
|
|
|
|
|
| 8 |
NIC offloads |
|
|
|
|
|
| 9 |
CPU affinity |
|
|
|
|
|
Use this table to decide what to do next. It keeps Suricata 8 packet drop performance tuning simple, because each symptom has one fix:
| What you see |
Likely cause |
First fix |
kernel_drops rise, one W# thread at 100%, others idle |
One big flow, or flows are not spread evenly |
Stream bypass, check cluster_flow |
kernel_drops rise, all W# threads near 100% |
Not enough CPU, or detection is too heavy |
Hyperscan, fewer rules, more cores |
| Drops in short bursts, CPU is not full |
Ring too small |
Raise ring-size |
kernel_drops is 0 but reassembly_gap is above 0 |
Loss before Suricata, bad checksums, or memcap |
Check ethtool -S, softnet drops, memcaps |
ssn_memcap_drop or segment_memcap_drop above 0 |
Memory limit too small |
Raise that memcap |
pkt_on_wrong_thread keeps growing |
Bad load balancing between queues |
RSS to 1 queue, or symmetric RSS |
softnet_drops grows |
Kernel queue full before capture |
Raise net.core.netdev_max_backlog, spread IRQs |
ethtool -S shows missed or no-buffer counters |
NIC ring too small, or CPU too slow to empty it |
Raise NIC RX ring, set IRQ affinity |
A good target is drops under 1% of packets, and ideally zero. If your sensor must not miss any traffic, aim for zero. Treat any drop as a bug you need to explain.
If the softnet number is the one that grows, try a larger kernel queue. This is a Linux setting, not a Suricata setting:
sudo sysctl -w net.core.netdev_max_backlog=50000
Make it permanent only after the test shows it helps:
echo "net.core.netdev_max_backlog = 50000" | sudo tee /etc/sysctl.d/99-suricata-backlog.confsudo sysctl --system
Step 8: Move the Result to a Real Server
The lab found the settings. Now you can apply them to the real interface. This is where Suricata 8 packet drop performance tuning becomes a real change on a real sensor.
To do this, find your capture interface name:
Edit the af-packet block. Use the real interface name, and keep the values that won in your lab:
1af-packet:2 - interface: ens33 threads: 64 cluster-id: 995 cluster-type: cluster_flow6 defrag: yes7 tpacket-v3: yes8 ring-size: 10000
Set HOME_NET back to your real networks. Test the file and check the service command:
sudo suricata -T -c /etc/suricata/suricata.yaml -vsystemctl cat suricata | grep ExecStart
The ExecStart line must use the AF_PACKET capture settings from your file. If it does not, edit the service so it does.
Finally, start the service and watch the live drop rate:
sudo systemctl enable --now suricatagrep '"event_type":"stats"' /var/log/suricata/eve.json | tail -n 1 | jq '.stats.capture | {kernel_packets, kernel_drops, drop_pct: (if .kernel_packets > 0 then (.kernel_drops / .kernel_packets * 100) else 0 end)}'
These numbers count from the time Suricata started. Check them at busy hours and after rule updates, not only at quiet times.
Also, your sensor needs to see real traffic. Use a SPAN/mirror port or a network TAP. If you tune an inline setup, keep your inline AF_PACKET settings and follow the inline guide linked in Round 1.
If your Suricata 8 packet drop performance tuning results show you need more cores or a faster port, you can run your sensor on a dedicated server with enough CPU and network speed. You can pick the number of CPU cores and the port speed to match your traffic.
Cleanup the Suricata Test Lab After Tuning
When you finish, you can remove the lab so it does not stay on the server:
sudo pkill iperf3sudo pkill -f "suricata -c"sudo ip netns del gen
Deleting the namespace also removes the virtual cable. Your result files stay in /var/log/suricata-lab/. Delete that folder when you no longer need it.
To get back the original config, run the command below:
sudo cp /etc/suricata/suricata.yaml.orig /etc/suricata/suricata.yaml
Conclusion
Best Suricata 8 packet drop performance tuning is simple. You must measure, change one thing, and measure again. Start with capture.kernel_drops, then check tcp.reassembly_gap, the memcap counters, and CPU use per thread. Also, keep your CSV file, which is the proof of what worked on your hardware.
We hope you enjoy this guide. For more tuning considerations, check the Suricata 8.0 documentation.