nftables

Monitoring nftables counters in production: packets, bytes, drop rate

September 12, 2026 · 8 min read
Illustration immersive du sujet : nftables counters

When you rely on nftables counters to track dropped packets, accepted connections, or rate-limit triggers in production, you expect the numbers to reflect reality—but do they? In 2026, operators managing high-throughput Linux firewalls still encounter scenarios where counter values lag, disappear, or flatly contradict kernel logs, turning troubleshooting into guesswork. This article dissects the most common nftables counter blind spots, explains why they occur at the kernel level, and shows you how to instrument your firewall stack—XDP, nftables, and conntrack—so metrics match the real packet flow hitting your interface.

nftables counters are implemented as per-rule or per-chain kernel objects that increment atomically every time a packet matches. On paper, this is simple: one packet, one increment. In practice, three layers of complexity interfere: early XDP drops never reach nftables, counters in base-chain hooks fire before connection-tracking state is finalized, and race conditions during map updates can silently reset counters if you script rule changes without flushing the ruleset cleanly. The result is a dashboard that shows 10k dropped packets while your actual interface received—and discarded at XDP—two million more.



Why nftables Counters Miss Packets Dropped Earlier in the Stack

The Linux network stack is a pipeline: physical NIC → XDP hook → Traffic Control (TC) → netfilter (nftables/iptables) → socket delivery. A counter in an nftables rule only increments if the packet survives every stage before netfilter's input, forward, or output hook. If your XDP program issues XDP_DROP, the packet vanishes before the kernel allocates an sk_buff; nftables never sees it, so your counter name drop_counter stays at zero even though the interface is shedding millions of packets per second.

This is by design—XDP exists precisely to reject attack traffic with sub-microsecond latency, long before expensive stateful operations. But it creates a counter blind spot: your nftables dashboard looks quiet while your NIC's hardware counters (visible in ethtool -S eth0 or ip -s link) climb rapidly. Operators who tune rate-limits or blacklist rules in nftables, unaware that the real bottleneck is an XDP program silently dropping everything, waste hours tweaking the wrong layer.

Solution: Instrument XDP with BPF Maps and Export Metrics

Unlike nftables counters, XDP programs maintain statistics in BPF maps—hash maps, per-CPU arrays, or LRU maps—that you poll from user space. PAKKT.io runs a single, centralized XDP program (the PAKKT Engine) per interface and writes per-rule packet counts, per-source-IP hit counters, and per-protocol drops into dedicated BPF maps. Every 30 seconds the Go agent reads those maps via bpf(BPF_MAP_LOOKUP_ELEM), timestamps the samples, and ships them to TimescaleDB for dashboard rendering and anomaly detection.

# Example: dumping the PAKKT Engine's rule-hit map
bpftool map dump name pakkt_rules
# Output (simplified):
# key: 0x01  value: pkts=2847392 bytes=0 action=XDP_DROP
# key: 0x02  value: pkts=182      bytes=0 action=XDP_PASS

Because PAKKT's XDP telemetry and nftables counters share the same time-series database, the dashboard can overlay both: you see XDP drops in one panel, nftables accept/drop counters in another, and conntrack table size in a third. When a surge appears only in the XDP panel, you know the traffic never reached nftables; when both panels spike, you know packets passed XDP but hit a stateful nftables rule or connection-tracking limit.



Race Conditions and Counter Resets During Live Rule Updates

nftables counters live inside rule objects in kernel memory. When you delete a rule with nft delete rule inet pakkt input handle 42, the kernel destroys the counter object; the accumulated value is gone. If your automation scripts rebuild the ruleset by flushing chains (nft flush chain inet pakkt input) and re-adding rules, every counter resets to zero—even if the logical firewall policy is identical. This produces a sawtooth pattern in your metrics: counters climb for minutes or hours, then flatline at the next configuration push, erasing forensic evidence of an attack or policy violation.

Worse, if two processes update the ruleset concurrently—say, a fail2ban timer adding a temporary block while your configuration-management tool applies the base policy—one transaction may succeed and the other silently fail or partially apply, leaving orphaned counters that no rule references. The nft list ruleset output looks correct, but nft list counters shows stale objects with outdated values.

Use Named Counters and Atomic Ruleset Replacement

Instead of anonymous per-rule counters, declare named counters as independent objects in the inet pakkt table. Named counters persist across rule deletions as long as the table exists, and multiple rules can reference the same counter:

nft add counter inet pakkt global_drop
nft add counter inet pakkt ssh_accept

nft add rule inet pakkt input tcp dport 22 counter name ssh_accept accept
nft add rule inet pakkt input counter name global_drop drop

When you rebuild the ruleset, load the new configuration into a temporary table, verify it with nft -c -f /tmp/new-ruleset.nft, then atomically swap tables or use nft -f with a single transaction that deletes old rules and adds new ones without flushing counters. PAKKT Integrations (Pterodactyl, public API) follow this pattern: the agent maintains a declarative shadow state and computes the minimal diff of nftables commands, reusing named counters so historical data remains intact across policy changes.

Pattern Counter Fate Use Case
Anonymous per-rule Destroyed on rule delete Short-lived debug rules
Named counter, stable rules Persists across rule edits Long-term baselines (SSH, HTTP)
Named counter, dynamic set One counter, many IPs in set Blacklist / whitelist with single metric


Connection-Tracking State vs. Counters: Timing and Visibility

nftables rules in the input or forward base chains fire before connection-tracking fully classifies the packet. A SYN packet on a new TCP flow has ct state new, but if that SYN is malformed or part of a SYN flood, conntrack may drop it internally after your counter has already incremented. Your counter name syn_new counts the SYN, but the connection never appears in conntrack -L, and the application never sees it—creating a discrepancy between firewall counters and application logs.

Similarly, if you rate-limit new connections with ct state new limit rate 100/second, packets beyond the 100/s threshold are dropped by the limit expression, not by an explicit drop rule. Unless you place a counter before the limit, those drops are invisible:

# Blind spot: limit drops are not counted
nft add rule inet pakkt input ct state new limit rate 100/second accept

# Visible: counter before limit
nft add rule inet pakkt input ct state new counter name new_conn limit rate 100/second accept
nft add rule inet pakkt input ct state new counter name new_conn_drop drop

The second snippet counts every new-connection attempt in new_conn, then the limit either accepts or falls through to the explicit drop rule, which increments new_conn_drop. Now your metrics show both accepted and rejected new connections, and you can graph the ratio to detect when an attack saturates your rate-limit.

Correlating nftables Counters with Conntrack Table Size

Another blind spot emerges when the conntrack table fills. The kernel parameter net.netfilter.nf_conntrack_max caps the number of tracked flows; once reached, new connection attempts are silently dropped—no nftables counter increments, no log entry unless you enabled nf_conntrack logging at the module level. Operators see legitimate traffic failing with no corresponding spike in firewall drop counters.

The fix is to monitor /proc/sys/net/netfilter/nf_conntrack_count alongside your nftables counters. PAKKT's Go agent scrapes this file every heartbeat and graphs it next to per-rule metrics. When nf_conntrack_count approaches nf_conntrack_max and your ct state new accept counter plateaus, you know conntrack exhaustion is the bottleneck, not a firewall rule. You can then either raise the limit, shorten timeouts (nf_conntrack_tcp_timeout_established), or offload stateless filtering to XDP to reduce the number of flows that reach conntrack.



Exporting and Auditing Counters in Production

nftables counters exist only in kernel memory; a reboot or systemctl restart nftables wipes them. To preserve historical trends, you must export counter values to persistent storage before any restart or ruleset flush. The simplest export is a cron job that runs nft -j list counters inet pakkt and writes JSON to a file or pushes it to a time-series database:

#!/bin/bash
# Export nftables counters to JSON (example cron every 60s)
nft -j list counters inet pakkt > /var/log/nft-counters-$(date +%s).json

For real-time dashboards, parse the JSON and insert rows into TimescaleDB, Prometheus, or InfluxDB. PAKKT's agent does this automatically: every 30 seconds it fetches both XDP map statistics and nftables counter objects, tags each sample with agent ID, rule ID, and timestamp, then batches the insert over mTLS to the central backend. The result is a unified view where you can query "show me drops per source country for rule 5 in the last hour" and get a blended answer from XDP and nftables layers.

Audit Logging Configuration Changes That Affect Counters

Even with persistent export, you need an audit trail of why a counter reset or disappeared. PAKKT maintains an internal audit log (visible in the panel under agent → Logs) that records every nftables command the agent executed, the diff applied, and the resulting counter state. When a DevOps engineer later asks "Why did the SSH accept counter drop to zero at 03:42 UTC?", the log shows the exact nft flush chain command, the user or automation that triggered it, and the new ruleset hash.

If you manage nftables manually, integrate nft calls into your configuration-management playbook (Ansible, Terraform, or shell scripts) with structured logging. Tag each transaction with a change ID and commit the counter snapshot before and after. This discipline transforms counters from ephemeral debug aids into forensic-grade telemetry.



Conclusion

nftables counters are powerful, but only if you account for the kernel pipeline that precedes them—XDP, Traffic Control, and connection-tracking—and the lifecycle quirks of rule objects. By instrumenting XDP with BPF maps, using named counters that survive rule churn, monitoring conntrack table size, and exporting counter snapshots to persistent storage, you close the visibility gaps that turn production incidents into mysteries. The result is a firewall stack where every dropped, rate-limited, or accepted packet is accounted for, from the NIC's DMA ring to the application socket, giving you ground truth for capacity planning, attack forensics, and compliance audits.



FAQ

Why do my nftables drop counters stay at zero during a DDoS, even though traffic stops reaching the application?

If you have an XDP program loaded on the interface, it processes packets before nftables and can issue XDP_DROP verdicts that never allocate an sk_buff. Those drops are invisible to nftables counters. Check ip -s link show dev eth0 for RX dropped frames, and use bpftool map dump to inspect XDP statistics. PAKKT surfaces both XDP and nftables metrics in a unified dashboard so you see the full picture.

Can I preserve nftables counter values across a ruleset reload without losing historical data?

Yes—declare counters as independent named objects (nft add counter inet pakkt my_counter) rather than embedding anonymous counters in rules. When you rebuild rules, reference the same named counter. Additionally, export counter values to a time-series database before any flush operation; PAKKT's agent does this every 30 seconds, ensuring no sample is lost even if the kernel object resets.

How do I count packets rejected by connection-tracking limits when nf_conntrack_max is exceeded?

The kernel drops new flows silently once the conntrack table is full; no nftables counter increments. Monitor /proc/sys/net/netfilter/nf_conntrack_count and compare it to nf_conntrack_max. When the ratio approaches 1.0 and your ct state new accept counter stops growing, conntrack exhaustion is the cause. Either raise the limit, reduce timeouts, or move stateless filtering to XDP to keep fewer flows in the conntrack table.

Protect your servers

Deploy PAKKT in 30 seconds

Dual-layer kernel protection. XDP + nftables. Driven from a central panel. 7-day free trial.