XDP / eBPF

Rate-limiting an IP at the kernel level with XDP and eBPF: complete tutorial

July 3, 2026 · 8 min read
Illustration immersive du sujet : rate-limit IP XDP eBPF

When you need to rate-limit an IP with XDP eBPF, you're addressing one of the most critical challenges in modern network security: absorbing volumetric attacks and malicious traffic at the earliest possible stage—before packets even reach the kernel's network stack. In 2026, as DDoS attacks grow more sophisticated and bandwidth-intensive, implementing XDP rate-limiting directly at the NIC driver layer provides sub-microsecond per-packet filtering with negligible CPU overhead. This tutorial walks you through the fundamentals of XDP-based rate-limiting, explains how eBPF maps enable dynamic rule management, and demonstrates practical deployment strategies for production Linux servers running kernel 5.x or higher.

XDP (eXpress Data Path) operates at the lowest software layer in the Linux networking stack, allowing you to process incoming packets immediately after they arrive at the network interface—before sk_buff allocation, before conntrack, before any iptables or nftables processing. This architectural advantage makes XDP the optimal technology for high-performance packet filtering and rate-limiting, capable of handling several million packets per second on commodity hardware.



Understanding XDP eBPF Rate-Limiting Architecture

XDP programs are written in restricted C, compiled to eBPF bytecode, and attached to a network interface. When a packet arrives, the XDP hook executes your eBPF program before the kernel allocates expensive data structures. The program inspects packet headers (Ethernet, IP, TCP/UDP) and returns one of five verdicts: XDP_PASS (continue to kernel), XDP_DROP (discard immediately), XDP_TX (bounce back), XDP_REDIRECT (send to another interface), or XDP_ABORTED (error state).

For rate-limiting, your XDP program must track packet counters per source IP address and enforce thresholds. Since XDP programs are stateless by design, you store state in eBPF maps—kernel-managed hash tables, arrays, or LRU caches shared between the eBPF program and userspace control plane. A typical rate-limit implementation uses:

  • BPF_MAP_TYPE_LRU_HASH to store per-IP packet counters with automatic eviction of least-recently-used entries
  • BPF_MAP_TYPE_ARRAY for global configuration (max PPS threshold, time window)
  • BPF_MAP_TYPE_PERCPU_HASH for high-concurrency scenarios to avoid lock contention across CPU cores

The XDP program reads the source IP from the packet, looks up the current counter in the map, increments it, and compares against the configured threshold. If exceeded, it returns XDP_DROP; otherwise, XDP_PASS. A separate userspace daemon periodically resets counters based on the time window (e.g., every second for per-second rate limits).

Challenges of Stateless XDP Rate-Limiting

XDP's stateless nature means you cannot trivially implement sliding-window algorithms or byte-rate limiting. Packet-per-second (PPS) rate-limiting is straightforward: count packets, reset periodically. However, byte-rate limiting requires maintaining cumulative byte counters and complex time-window logic, which adds latency and CPU cost. For byte-rate enforcement, the kernel's nftables meter feature (stateful, operates post-XDP) is more appropriate.

Additionally, XDP programs have strict resource limits: maximum 1 million verified instructions, no unbounded loops, limited stack size (512 bytes), and restricted kernel helper functions. Your rate-limit logic must be efficient, using bitwise operations and avoiding division where possible.


Close-up photorealistic view of illuminated fiber optic cables connecting to a high-density network switch in a modern datacenter, cyan and blue LED indicators glowing, with Ethernet ports visible and shallow depth of field emphasizing network interface hardware


Implementing XDP Rate-Limiting: Practical Code Example

Below is a simplified XDP eBPF program skeleton that performs per-source-IP rate-limiting. This example assumes a fixed 10,000 PPS threshold per IP and uses an LRU hash map to track counters:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

struct pkt_counter {
    __u64 count;
    __u64 last_reset;
};

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 65536);
    __type(key, __u32);   // Source IP
    __type(value, struct pkt_counter);
} ip_counters SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u64);  // max_pps threshold
} config SEC(".maps");

SEC("xdp")
int xdp_rate_limit(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_DROP;
    
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;
    
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_DROP;
    
    __u32 src_ip = ip->saddr;
    __u64 now = bpf_ktime_get_ns();
    
    struct pkt_counter *counter = bpf_map_lookup_elem(&ip_counters, &src_ip);
    if (!counter) {
        struct pkt_counter new_counter = {1, now};
        bpf_map_update_elem(&ip_counters, &src_ip, &new_counter, BPF_ANY);
        return XDP_PASS;
    }
    
    // Reset counter every 1 second (1e9 nanoseconds)
    if (now - counter->last_reset > 1000000000ULL) {
        counter->count = 1;
        counter->last_reset = now;
        return XDP_PASS;
    }
    
    __u32 key = 0;
    __u64 *max_pps = bpf_map_lookup_elem(&config, &key);
    if (!max_pps)
        return XDP_PASS;
    
    if (counter->count >= *max_pps)
        return XDP_DROP;
    
    counter->count++;
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Compile this program using Clang with the -target bpf flag and load it onto an interface with ip link set dev eth0 xdp obj rate_limit.bpf.o sec xdp. You can inspect the map contents with bpftool map dump name ip_counters and update the config map from userspace to change the threshold dynamically:

bpftool map update name config key 0 0 0 0 value 10000 0 0 0 0 0 0 0

Integrating with PAKKT Engine

The PAKKT.io platform abstracts this complexity by providing a production-grade XDP engine that supports up to 256 simultaneous rules per interface. Instead of writing raw eBPF code, you define rules via the centralized dashboard or API, specifying parameters like port range, protocol, rule type (block / rate_limit / allow_only), max_pps, and packet size filters. The PAKKT agent compiles and loads the optimized XDP program, manages BPF map updates, and synchronizes configuration across your server fleet with mTLS-secured 30-second heartbeats.

For example, to rate-limit UDP port 19132 (Minecraft Bedrock) to 5,000 PPS per source IP while blocking TCP SYN floods on port 25565, you'd create two rules in the PAKKT dashboard. The agent translates these into efficient eBPF map entries, avoiding the need to reload the entire XDP program. This architecture ensures zero packet loss during rule updates and maintains sub-microsecond per-packet latency even under attack conditions.


Photorealistic server rack in a professional datacenter environment, close-up of a 1U Linux server with green status LEDs, Ethernet cables neatly organized with blue cable management, ambient cyan lighting, shallow depth of field focusing on network ports and indicator lights


Complementary nftables Stateful Rate-Limiting

While XDP excels at stateless, high-volume packet filtering, certain rate-limiting scenarios require connection tracking and stateful inspection. For instance, limiting new TCP connections per second from a single IP, or enforcing per-connection byte-rate limits, demands the full conntrack state machine. This is where nftables complements XDP.

nftables operates after the XDP layer, in the kernel's netfilter hooks. It supports powerful features like ct state matching, connection tracking helpers, and the limit / meter constructs for dynamic, per-flow rate-limiting. A common pattern is to use XDP for coarse-grained, high-PPS filtering (dropping obvious attack traffic before it consumes conntrack resources) and nftables for fine-grained, stateful policies.

Example: limit new TCP connections to port 25565 at 50 per second per source IP using nftables meter:

nft add table inet pakkt
nft add chain inet pakkt input { type filter hook input priority filter \; policy accept \; }
nft add rule inet pakkt input tcp dport 25565 ct state new \
  meter conn_limit { ip saddr limit rate 50/second burst 10 } accept
nft add rule inet pakkt input tcp dport 25565 ct state new drop

This ruleset creates a meter named conn_limit that tracks per-source-IP connection attempts. If an IP exceeds 50 new connections per second (with a burst allowance of 10), subsequent connection attempts are dropped. The ct state new match ensures only SYN packets (new connections) are counted, not established traffic.

Dual-Layer Rate-Limiting Strategy

Best practice: configure XDP to enforce a global PPS ceiling (e.g., 100,000 PPS total, or 10,000 PPS per source IP) to protect against volumetric floods. Then, apply nftables rules for protocol-specific rate-limits (TCP connection rate, ICMP rate, UDP rate per port) to catch sophisticated attacks that operate within the XDP threshold but still abuse application resources. PAKKT Integrations (Pterodactyl, public API) allow you to automate this dual-layer configuration via API calls, synchronizing XDP and nftables rules based on real-time attack telemetry.

The PAKKT platform provisions nftables rules in an isolated inet pakkt table, ensuring zero conflict with Docker (which uses the nat table), fail2ban (typically filter table), or iptables-persistent. This clean separation prevents rule-order bugs and simplifies troubleshooting.



Monitoring and Observability for XDP Rate-Limiting

Effective rate-limiting requires continuous monitoring. You need visibility into which IPs are being rate-limited, how many packets are dropped versus passed, and whether your thresholds are correctly tuned. eBPF maps expose counters that userspace tools can read, but raw bpftool output is unwieldy for operational dashboards.

The PAKKT agent collects per-rule metrics (packets passed, packets dropped, bytes processed) every 30 seconds and streams them to the centralized panel, which stores time-series data in TimescaleDB. The dashboard renders real-time graphs of traffic per port, per rule, and per source country (GeoIP lookup). You can drill down to see the top source IPs hitting a specific rate-limit rule, export audit logs for compliance, and configure alerts when thresholds are breached.

For example, if your UDP port 27015 (Source engine) rate-limit rule suddenly drops 90% of traffic, the GeoIP world map might reveal a botnet concentrated in a specific ASN. You can then add that ASN's IP ranges to the dual-layer blacklist (XDP BPF map + nftables set), which the agent synchronizes automatically across all protected servers.

Performance Impact and Tuning

XDP rate-limiting introduces minimal overhead. In native XDP mode (driver-level), per-packet processing typically adds less than 1 microsecond of latency. CPU utilization remains under 1% for moderate traffic loads (tens of thousands of PPS) on modern multi-core systems. However, excessive map lookups or large hash tables can degrade cache locality. Keep your LRU hash map size reasonable (65,536 entries covers most scenarios) and use per-CPU maps if you exceed 1 million PPS.

The PAKKT Go agent itself consumes fewer than 5 MB of RAM and less than 1% CPU under normal operation, including mTLS heartbeat, SHA256 self-update checks, and garble obfuscation for binary protection. This lightweight footprint ensures your game server, web application, or database workload retains full resource access.

For advanced tuning, consult the eBPF.io documentation for best practices on map sizing, verifier optimization, and co-re (Compile Once, Run Everywhere) portability across kernel versions.



Conclusion

Rate-limiting an IP with XDP eBPF provides unmatched performance and efficiency for protecting Linux servers against volumetric attacks and abusive traffic. By processing packets at the NIC driver layer before kernel stack allocation, XDP achieves sub-microsecond latency and multi-million PPS throughput on commodity hardware. Combining stateless XDP rate-limiting for high-volume filtering with stateful nftables rules for protocol-specific enforcement creates a robust, multi-layered defense. Operational tooling like the PAKKT platform simplifies deployment, centralizes monitoring, and enables dynamic rule updates without service interruption, making enterprise-grade kernel-level protection accessible at €3 per agent per month.



FAQ

Can I rate-limit based on byte rate instead of packet rate in XDP?

XDP programs are stateless and optimized for packet-level decisions. While you can track byte counters in eBPF maps, implementing accurate byte-rate limiting with sliding windows adds significant complexity and latency. For byte-rate enforcement, use nftables limit or meter constructs, which operate post-XDP with full connection tracking and can enforce per-connection or per-source-IP byte rates efficiently.

How do I update XDP rate-limit thresholds without reloading the program?

Store your configuration (max_pps, time window) in a BPF_MAP_TYPE_ARRAY or BPF_MAP_TYPE_HASH map. Your XDP program reads thresholds from the map at runtime. Use bpftool map update from userspace to change values dynamically. The PAKKT platform automates this via API or dashboard, applying threshold changes across all agents instantly without recompiling or reloading the XDP program.

What kernel version is required for XDP rate-limiting with eBPF maps?

XDP was introduced in Linux kernel 4.8, but production-grade features (LRU maps, helper function stability, per-CPU maps) stabilized in kernel 4.18+. For optimal compatibility and performance, use kernel 5.x or higher. The PAKKT agent requires kernel 5.x minimum to ensure reliable XDP attachment, map operations, and BTF (BPF Type Format) support for portable bytecode across distributions.

Protect your servers

Deploy PAKKT in 30 seconds

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