Per-IP rate limiting with nftables: properly configuring rate limits
When nftables rate limiting is implemented correctly, server administrators can defend their infrastructure against packet floods, brute-force login attempts, and amplification attacks without sacrificing legitimate traffic. In 2026, per-IP rate limiting remains the most surgical approach to throttle abusive sources while keeping clean connections fast, and nftables offers the stateful tracking and meter primitives required to enforce these policies at the kernel level with minimal overhead.
This guide walks you through the principles, syntax, and real-world examples of configuring per-IP rate limits in nftables, explains how stateful connection tracking integrates with meters and dynamic sets, and demonstrates how to layer these rules alongside XDP for comprehensive multi-stage protection.
Why Per-IP Rate Limiting Matters in Modern Network Defense
Traditional port-based or global packet-per-second (PPS) limits protect bandwidth but lack granularity: a single malicious source can exhaust your quota while legitimate users from other IPs suffer collateral blocking. Per-IP rate limiting isolates each source address into its own bucket, applying independent counters and thresholds so that one attacker cannot poison the well for everyone else.
Stateful firewalls like nftables track connection state (NEW, ESTABLISHED, RELATED) and can apply rate limits only to new connection attempts—precisely where SYN floods, UDP amplification, and brute-force SSH scans concentrate their traffic. By combining ct state new with the meter statement, you create a per-source-IP token bucket that refills over time, allowing bursts up to a defined ceiling while penalizing sustained abuse.
In high-throughput environments—game servers, API endpoints, VoIP gateways—this granularity is non-negotiable. A legitimate player's IP should never be throttled because an attacker on a different continent is flooding port 25565. Tools like PAKKT.io extend this philosophy by pairing nftables stateful rules with a single XDP program per interface, enabling sub-microsecond L2/L3 drops before packets even reach the nftables hook, then handing survivors to connection-tracked rate limits for final adjudication.

Understanding nftables Meters and Connection Tracking
The Meter Statement: Dynamic Per-IP Buckets
The meter statement in nftables creates an anonymous or named set that stores a key (typically ip saddr) alongside a rate-limit counter. Each time a packet arrives, nftables hashes the source IP, looks up the corresponding bucket, and checks whether the token count permits another packet. If the limit is exceeded, the rule verdict (usually drop or reject) is applied; otherwise, the packet proceeds and the counter increments.
nft add rule inet pakkt input tcp dport 22 ct state new meter ssh_limiter { ip saddr limit rate 5/minute } accept
This rule permits up to five new SSH connections per minute from any given source IP. The meter ssh_limiter is dynamically populated; no pre-configuration of IP addresses is required. Entries expire automatically after the timeout window, freeing memory for new sources.
Connection Tracking and Stateful Filtering
By prefixing rate-limit rules with ct state new, you ensure only the first packet of each connection is counted. Subsequent packets in ESTABLISHED or RELATED states bypass the meter entirely, preserving performance for long-lived TCP sessions and reducing false positives. This design is critical for protocols like HTTPS, game server query/response loops, and database replication streams.
nft add rule inet pakkt input udp dport 19132 ct state new meter bedrock_limiter { ip saddr limit rate 100/second burst 200 packets } accept
nft add rule inet pakkt input ct state established,related accept
Here, Minecraft Bedrock clients on UDP 19132 are allowed up to 100 new packets per second per IP, with a burst allowance of 200 packets to accommodate legitimate connection handshakes. Once the session is established, all further packets flow through the ct state established,related fast-path.
Burst Sizes and Token Bucket Semantics
The optional burst parameter defines the maximum number of tokens a bucket can accumulate. Without it, the burst defaults to five. In practice, interactive applications (SSH, web forms) benefit from small bursts (5–10), while bulk protocols (game server join floods, API batch requests) may require 50–500 to avoid punishing legitimate spikes.
Choose burst sizes based on measured baseline traffic: capture a normal peak with nft monitor trace or tcpdump, then set the burst ceiling 20–30% above that maximum to leave headroom for organic growth.

Step-by-Step Configuration Examples
Basic Per-IP Rate Limit for a Single Port
Protect SSH (port 22) against brute-force attacks by limiting each source IP to three new connection attempts per minute:
nft add table inet pakkt
nft add chain inet pakkt input { type filter hook input priority filter \; policy drop \; }
nft add rule inet pakkt input ct state established,related accept
nft add rule inet pakkt input iif lo accept
nft add rule inet pakkt input tcp dport 22 ct state new meter ssh_rate { ip saddr limit rate 3/minute } accept
nft add rule inet pakkt input tcp dport 22 drop
Any IP exceeding three new SSH connections in sixty seconds will have subsequent SYN packets dropped until the minute window resets. Legitimate administrators who authenticate successfully on the first or second try will never hit the ceiling.
Multi-Port Web Application Defense
For HTTP/HTTPS services, apply a combined meter across ports 80 and 443 to prevent attackers from splitting requests between protocols:
nft add rule inet pakkt input tcp dport { 80, 443 } ct state new meter http_limiter { ip saddr limit rate 50/second burst 100 packets } accept
Each client IP may initiate up to fifty new TCP connections per second, with a burst allowance of one hundred. This configuration tolerates browser parallelism (modern browsers open 6–12 connections per domain) while blocking HTTP flood bots that spawn thousands of connections per second.
Game Server Protection with Protocol-Specific Rules
Minecraft Java Edition (TCP 25565) and query protocol (UDP 25565) require separate handling:
nft add rule inet pakkt input tcp dport 25565 ct state new meter mc_tcp_limiter { ip saddr limit rate 10/second burst 20 packets } accept
nft add rule inet pakkt input udp dport 25565 meter mc_udp_limiter { ip saddr limit rate 50/second burst 100 packets } accept
TCP connections (player logins) are rare and expensive; UDP queries (server list pings) are frequent and cheap. The asymmetric limits reflect operational reality. Integrations like PAKKT Integrations (Pterodactyl, public API) automate the deployment of these rule templates across fleets of game servers, synchronizing port configurations and per-IP whitelists from a central dashboard.
Advanced: Layered Rate Limits with Multiple Meters
Combine global and per-IP meters to enforce both aggregate infrastructure capacity and per-client fairness:
nft add rule inet pakkt input tcp dport 3306 meter mysql_global { limit rate 500/second } accept comment "Global MySQL PPS cap"
nft add rule inet pakkt input tcp dport 3306 ct state new meter mysql_per_ip { ip saddr limit rate 10/second } accept comment "Per-IP new connection limit"
nft add rule inet pakkt input tcp dport 3306 drop
The first rule ensures total inbound MySQL traffic never exceeds 500 packets per second, protecting the database process from resource exhaustion. The second rule subdivides that capacity fairly: no single client can monopolize more than ten new connections per second. Clients exceeding the per-IP threshold are dropped even if global capacity remains.
Monitoring, Tuning, and Troubleshooting Per-IP Meters
Inspecting Active Meters
List all meters and their current entries:
nft list meters inet pakkt
Each meter displays its name, key expression (ip saddr), rate parameters, and the number of active buckets. To see individual IP entries and their token counts, query the underlying anonymous set:
nft list meter inet pakkt ssh_rate
Entries are ephemeral and expire after the rate window plus a grace period; you will not see IPs that have been idle for several minutes.
Adjusting Limits Without Downtime
To change a rate limit, delete the old rule by its handle and insert a new one with updated parameters:
nft --handle list chain inet pakkt input
nft delete rule inet pakkt input handle 12
nft add rule inet pakkt input tcp dport 22 ct state new meter ssh_rate { ip saddr limit rate 5/minute } accept
The meter retains its state across rule modifications as long as the meter name remains unchanged. If you rename the meter or delete the chain, all counters reset.
Logging and Alerting on Rate-Limit Violations
Insert a logging rule immediately before the drop to capture denied packets:
nft add rule inet pakkt input tcp dport 22 ct state new meter ssh_rate { ip saddr limit rate over 3/minute } log prefix "SSH rate exceeded: " drop
The limit rate over syntax triggers the verdict only when the threshold is breached. Logs appear in /var/log/kern.log or journalctl -k, tagged with the prefix. For centralized monitoring, forward these logs to a SIEM or parse them with a time-series collector. Platforms like PAKKT.io aggregate per-port metrics in TimescaleDB, rendering real-time graphs of blocked vs. accepted packets, top offender IPs, and GeoIP heatmaps on a world map—streamlining triage during an ongoing attack.
Common Pitfalls and Solutions
- Forgetting
ct state established,related accept: Without this rule above your rate limiters, even established connections will be metered on every packet, causing severe performance degradation and false drops. - Overloading the conntrack table: High PPS environments can exhaust
nf_conntrack_max. Increase the limit viasysctl -w net.netfilter.nf_conntrack_max=1048576and tunenf_conntrack_bucketsaccordingly. - Conflicting chains: If Docker, fail2ban, or legacy iptables rules exist, isolate nftables in a dedicated
inet pakkttable with an explicit priority (e.g.,priority filter - 10) to evaluate before other hooks. - Zero burst tolerance: Omitting the
burstkeyword defaults to five packets. Protocols with bursty handshakes (QUIC, WebRTC) may require burst values of 50–200.
For deployments requiring sub-microsecond pre-filtering before nftables even sees the packet, layering an XDP program in front of the stateful firewall is essential. XDP operates at the earliest possible kernel hook—immediately after the NIC driver—and can drop malformed packets, enforce simple port/protocol allow-lists, or apply stateless PPS caps using BPF maps. The PAKKT.io engine exemplifies this architecture: a single XDP program per interface handles up to 256 simultaneous rules (port ranges, protocol filters, global PPS ceilings), while the complementary nftables layer in an isolated inet pakkt table applies stateful per-IP rate limits with conntrack and meters.
Integrating nftables Rate Limiting with XDP for Multi-Layer Defense
Modern DDoS mitigation demands defense in depth. XDP (eXpress Data Path) runs eBPF programs at the NIC driver level, processing packets before the kernel allocates socket buffers—orders of magnitude faster than iptables or nftables. However, XDP is stateless: it cannot track TCP handshakes or per-connection rate limits. Conversely, nftables excels at stateful filtering but incurs higher per-packet CPU cost.
The optimal strategy places a coarse XDP filter at the network edge to discard obviously malicious traffic—spoofed source IPs, packets below minimum size, ports not in the service allow-list—then passes survivors to nftables for fine-grained per-IP rate limiting and connection tracking. This two-stage pipeline balances raw throughput with policy richness.
Example Workflow
- XDP stage: Enforce a global 2 million PPS ceiling, drop packets smaller than 40 bytes (likely amplification fragments), and allow only ports 22, 80, 443, 25565. All other traffic is dropped at line rate with negligible latency.
- nftables stage: Apply per-IP meters to ports 22 (3/min), 80/443 (50/sec burst 100), and 25565 (10/sec). Use
ct state established,related acceptto fast-path legitimate sessions. - Logging and telemetry: Export XDP drop counters via BPF maps (read by user-space collector) and nftables log rules to a central dashboard for real-time visibility.
Deploying this stack manually requires compiling eBPF programs with Clang/LLVM, managing BPF map lifecycle with bpftool, and synchronizing nftables rulesets across server fleets—a weeks-long engineering project. Managed platforms reduce this to a single-agent installation: the PAKKT Go agent (mTLS, 30-second heartbeat, SHA256 self-update, garble obfuscation) loads the unified PAKKT Engine XDP object, populates BPF maps from centralized policy, and renders the nftables ruleset in the inet pakkt table—all with less than 1% CPU overhead and under 5 MB memory footprint.
Kernel and Hardware Requirements
XDP requires Linux kernel 5.0 or higher with CONFIG_BPF_SYSCALL and CONFIG_XDP_SOCKETS enabled. Most modern distributions (Ubuntu 20.04+, Debian 11+, Rocky Linux 8+) satisfy these prerequisites. NIC driver support varies: mlx5 (Mellanox), i40e (Intel), ixgbe (Intel 10GbE), and virtio_net (virtual) provide native XDP; others fall back to generic XDP mode with reduced performance. Verify your driver with:
ethtool -i eth0 | grep driver
ip link set dev eth0 xdp off
ip link set dev eth0 xdp obj pakkt_engine.bpf.o sec xdp
If the second command succeeds without error, your NIC supports XDP. The official kernel.org XDP documentation maintains a compatibility matrix for major hardware vendors.
Conclusion
Per-IP rate limiting in nftables transforms stateful connection tracking into a precise surgical instrument against abuse, isolating each source address into independent token buckets while preserving full throughput for legitimate clients. By layering meters with connection state filtering, burst allowances, and optional global caps, administrators gain the granularity required to defend SSH, web, and game server workloads against brute-force and amplification attacks. When combined with XDP's line-rate pre-filtering, this two-stage architecture delivers both sub-microsecond drop latency and rich stateful policy—kernel-level protection that scales from single VPS instances to global server fleets.
FAQ
Can I apply different rate limits to the same port based on source IP subnet?
Yes. Define multiple rules with ip saddr match conditions and distinct meter names. For example, allow a trusted /24 subnet 100/sec while limiting all others to 10/sec. Place the more specific rule first; nftables evaluates rules top-to-bottom and stops at the first match unless you use continue or queue verdicts.
How do nftables meters interact with conntrack table size limits?
Meters store per-IP counters independently of conntrack entries. However, every accepted TCP or stateful UDP flow still consumes a conntrack slot. If your rate limit permits 10,000 new connections per second globally, ensure nf_conntrack_max is sized for peak concurrency (e.g., sysctl -w net.netfilter.nf_conntrack_max=1048576). Monitor current usage with conntrack -C and adjust nf_conntrack_buckets to nf_conntrack_max / 4 for optimal hash performance.
What happens to existing connections when I modify a meter rate limit?
Changing the rate or burst parameter in a rule does not disrupt established connections tracked by conntrack—they continue flowing via the ct state established,related accept fast-path. Only new connection attempts (ct state NEW) are subject to the updated meter. The meter's internal token counters persist as long as the meter name and key expression remain unchanged; renaming the meter or deleting the chain resets all buckets.
Deploy PAKKT in 30 seconds
Dual-layer kernel protection. XDP + nftables. Driven from a central panel. 7-day free trial.