In FortiGate – High CPU and Memory load the root cause traced back to sessions that never get hardware-offloaded to the NPU/SPU — DNS and ICMP being the two protocols of interest, since they never leave the CPU path regardless of volume. This note is the workflow for actually finding which host is generating that non-offloaded load, and where it’s going, instead of just knowing “DNS is the culprit” in the abstract.
Why this matters
A FortiGate happily offloads the bulk of steady-state TCP traffic to hardware, so get system performance status showing high CPU with a modest total session count usually means a disproportionate share of sessions are stuck on the CPU path. DNS (UDP/53) sessions in particular carry a helper=dns-udp and are never NPU-offloaded:
session info: proto=17 proto_state=01...policy_dir=0 tunnel=/ helper=dns-udp vlan_cos=0/255state=may_dirty npu synced netflow-origin netflow-reply...npu info: flag=0x00/0x00, offload=0/0, ips_offload=0/0, epid=0/0, ipid=0/0, vlan=0x0000/0x0000vlifid=0/0, vtag_in=0x0000/0x0000 in_npu=0/0, out_npu=0/0, fwd_en=0/0, qid=0/0no_ofld_reason:ofld_fail_reason(kernel, drv): none/not-established, none(0)/none(0)npu_state_err=00/04
offload=0/0 and ofld_fail_reason: not-established are the tells: this session type is not eligible for offload at all, not merely failing to offload. One host doing this a handful of times a minute is nothing; one host doing it tens of thousands of times because it lacks local DNS caching becomes CPU load.
Workflow
1. Filter the session table to the suspect host
diagnose sys session filter cleardiagnose sys session filter src <HOST_IP>diagnose sys session list
Capture the full output (SSH session logging, or execute log filter style redirection to a file) rather than reading it on-screen — the next step needs it as text.
2. Keep only non-offloaded sessions
Pull out the blocks where offload=0/0 appears, and from each such block the destination tuple on the hook=pre dir=org line:
hook=pre dir=org act=noop <SOURCE_IP>:<SOURCE_PORT>-><DEST_IP>:<DEST_PORT>(0.0.0.0:0)
A quick way to do this once the session dump is saved to a file, on a workstation with standard Unix tools:
grep -B20 'offload=0/0' session-dump.txt \ | grep 'hook=pre dir=org' \ | sed -E 's/.*->([0-9.]+:[0-9]+)\(.*/\1/'
(Adjust the -B context depending on how many lines separate the hook=pre line from the npu info line in your FortiOS version — check one block manually first.)
3. Sort and count by destination
sort dest-list.txt | uniq -c | sort -rn > <HOST_IP>-no-offload-dest-sorted-counted.txt
The result is a ranked list of count destination_ip:port. In practice this immediately separates the signal from the noise:
- A handful of low-count entries on ports like
10050(Zabbix agent),1433(MSSQL),443are normal background chatter — monitoring and app traffic that happens to be UDP or otherwise non-offloadable in small volumes. - A small number of destinations on port
53with counts an order of magnitude higher than everything else is the actual finding: a client asking the same DNS server(s) far more often than it should, exactly the missing-local-caching pattern described in the CPU/memory post.
4. Cross-check against the DoS anomaly log
If the same destination(s) also show up as dstip in diagnose ips anomaly list / the anomaly log (see Fortigate DOS Protection) with attack="udp_flood" or attack="udp_dst_session" on dstport=53, that confirms the non-offloaded DNS traffic from this host is a meaningful contributor to the DoS-policy hits, not just CPU noise.
Takeaway
“High CPU, sessions stuck on softirq” tells you that something isn’t offloading. Filtering the session table per suspect host, keeping only offload=0/0 entries, and counting by destination tells you which host and which server — which is the difference between fixing a symptom (raise the DoS threshold) and fixing a cause (fix the client’s DNS caching, or move it behind a local resolver).
