+ When you run a UDP BitTorrent tracker behind Docker bridge networking, the Linux kernel + creates conntrack (connection tracking) entries for UDP flows that pass through Docker's + NAT layer. Under sustained tracker load those entries accumulate faster than they expire, + the conntrack table fills up, and the kernel starts silently dropping packets. +
++ The result is intermittent UDP timeouts with a characteristic self-recovery + cycle: the table fills, a probe gets dropped, entries expire, the table drains, the next probe + succeeds, and the cycle repeats. The application log is completely silent. No error, no counter, + no warning — just unexplained timeout spikes on your uptime monitor. +
++ This post documents the mechanism behind the problem, how to diagnose it, the fix, and a + reboot-persistence trap that trips many operators. +
+ ++ The first occurrence was on the original + torrust/torrust-demo hosted on + DigitalOcean. UDP uptime on + newTrackon had been fluctuating and eventually + dropped to around 60 % at peak. The investigation is documented in + torrust/torrust-demo#26. +
+
+ The kernel journal confirmed: nf_conntrack: table full, dropping packet with
+ 20 million+ early_drop events on CPU 3. After increasing
+ nf_conntrack_max, UDP uptime on
+ newTrackon recovered to
+ 99.2 %.
+
+ A few months later, in June 2025, the same DigitalOcean server filled the conntrack table
+ again (uptime back down to about 90 %, with fresh
+ nf_conntrack: table full, dropping packet messages and tens of millions of
+ early_drop events on CPU 3). The follow-up investigation in
+ torrust/torrust-demo#72
+ tried to go further than just raising the ceiling and disable conntrack for the tracker port
+ altogether using NOTRACK rules. As the
+ Alternative Approaches section below describes in detail, that
+ attempt failed in our Docker setup — even after switching the tracker to
+ --network=host mode — and ultimately required restoring a server backup. We kept
+ the sysctl tuning and migrated the demo to Hetzner shortly afterwards.
+
+ In April 2026 we migrated the + Torrust Tracker Demo to + Hetzner and resized the server from a CCX23 (4 vCPU, 16 GB RAM) to a CCX33 (8 vCPU, 32 GB + RAM) to improve performance. The opposite happened: UDP uptime the day after the resize + was + 83.9 %, down from 92.2 % before the resize. +
++ As we explain in the symptom section below, a larger server can make things + worse: more processing power means more requests per second, which fills the + conntrack table faster and increases the drop rate. +
+
+ Investigation (tracked in
+ torrust/torrust-tracker-demo#21) found nf_conntrack_count = nf_conntrack_max = 262144 — the table
+ completely full — with 2478 "table full" messages in dmesg.
+
+ The fix was applied on 2026-04-20 (see + torrust/torrust-tracker-demo PR #22) with all three parameters and the module pre-load. We are monitoring + newTrackon for recovery data. +
+ +dmesg and zero IPv4
+ UdpRcvbufErrors. The fix held across a server reboot and at peak load (~750
+ UDP req/s, ~2 000 HTTP req/s). Before the fix, UDP uptime had been as low as 83.9 % on the
+ day the conntrack table first filled (262 144 / 262 144 entries).
+ + If you run a UDP tracker and observe any of the following on an uptime monitor such as + newTrackon, you may be hitting conntrack exhaustion: +
+netstat / ss socket counters.
+ The standard places you look for dropped packets do not show this problem:
+ss -u -s and netstat -su show socket-level drops, not kernel-level
+ conntrack drops. They will not increment.
+ iptables / ufw log rules fire on
+ packets that reach the firewall. A packet dropped by the conntrack subsystem before the firewall
+ never appears in those logs.
+
+ The primary evidence is in dmesg and conntrack counters in
+ /proc/sys/net/netfilter/.
+
+ When we investigated the second occurrence on Hetzner, we found
+ nf_conntrack_count = nf_conntrack_max = 262144 — the table was completely
+ full at the moment of inspection — and 2478 "table full" drop messages in
+ dmesg.
+
+ When you publish a UDP port in Docker (-p 6969:6969/udp), Docker installs a
+ DNAT (Destination Network Address Translation) rule in iptables. This rule
+ rewrites the destination address of every inbound packet from the host's public IP to the
+ container's private bridge IP.
+
+ NAT requires connection tracking. The kernel must remember which packets were rewritten so + it can apply the reverse translation to outbound replies. For each new UDP "flow" (unique + source IP + source port combination), the kernel creates a conntrack entry. +
+ ++ Unlike TCP, UDP has no handshake. The kernel cannot know when a UDP exchange is + "finished", so each entry persists until a configurable timeout expires: +
++ A BitTorrent tracker announce is a request–response exchange, so entries are classified as + bidirectional with the 120-second timeout. Each unique client IP/port pair that sends an + announce holds a conntrack entry for two full minutes. +
+ ++ The minimum conntrack table size needed to handle your request rate without dropping + packets is: +
++ minimum_table_size = requests_per_second × udp_stream_timeout_seconds +
+
+ With default settings (udp_timeout_stream = 120 s) and a table size of 262
+ 144 entries:
+
+ That sounds large, but BitTorrent clients re-announce every 30–60 minutes from a rotating + pool of ports. A tracker with tens of thousands of active torrents, each with dozens of + peers, easily exceeds this rate at peak times. +
++ Reducing the stream timeout to 15 seconds multiplies the effective capacity by 8× without + changing the table size: +
++ Combining a larger table with a shorter timeout gives significant headroom even on a busy + public tracker. +
+ +
+ Create (or edit) /etc/sysctl.d/99-conntrack.conf with the following content
+ (the deployed version for the Torrust Tracker Demo is at
+ server/etc/sysctl.d/99-conntrack.conf):
+
Apply the settings immediately without rebooting:
+ +Verify that the new values are active:
+ +nf_conntrack_max from your actual request rate using the formula in the
+ previous section. Raising the table ceiling increases kernel memory usage (roughly 300–400
+ bytes per entry). At nf_conntrack_max = 1 048 576 that is ≈ 384 MB of kernel memory
+ reserved for the conntrack table — trivial on a 32 GB server, but worth budgeting for on a 1–2
+ GB VPS.
+
+ When you raise nf_conntrack_max by an order of magnitude, the
+ hash bucket count does not auto-scale. The default is around 65 536
+ buckets; if you keep that while raising the ceiling to 1 048 576, every lookup walks long
+ collision chains and table operations degrade from O(1) toward O(n). The recommended ratio
+ is roughly
+ nf_conntrack_max / 4 to nf_conntrack_max / 8.
+
+ You can tune buckets with the nf_conntrack_buckets sysctl (writeable in the
+ initial network namespace) or set the module parameter hashsize for early-boot
+ consistency.
+
+ The nf_conntrack_udp_timeout* values are kernel-wide — they apply to every
+ UDP flow on the host, not only to tracker traffic. A 15-second stream timeout is
+ appropriate for request–response protocols like a BitTorrent tracker, DNS resolver, or
+ QUIC server, but it can be aggressive for long-lived UDP services such as WireGuard,
+ IPsec, VoIP/SIP gateways, or long-running game servers. If you co-host such services,
+ either keep the default 120 s or use
+ NOTRACK rules (see the
+ Alternative Approaches section) to exempt them from connection tracking
+ entirely.
+
+ This is where many operators get burned: you apply the fix, it works perfectly, you reboot + the server, and the problem silently comes back. +
+
+ The net.netfilter.nf_conntrack_* sysctl keys only exist after the
+ nf_conntrack kernel module has been loaded. The module is loaded by Docker
+ when Docker starts. However, systemd applies sysctl configuration at boot
+ before Docker runs — so when systemd reads
+ /etc/sysctl.d/99-conntrack.conf, the keys do not exist yet and the settings
+ are silently skipped.
+
The fix is to instruct the kernel to pre-load the module during boot:
+ +
+ With this in place, the module is loaded early in the boot sequence, the sysctl keys exist
+ when systemd applies sysctl.d, and the settings take effect before Docker
+ starts.
+
/etc/modules-load.d/conntrack.conf, the settings will not survive a reboot
+ even though sysctl --system confirms they are active on the running system.
+ + After the next reboot, verify both that the module is loaded and that the values are + correct: +
+ ++ Tuning conntrack raises the ceiling, but the most fundamental fix is to stop creating + conntrack entries for tracker traffic in the first place. There are three approaches worth + knowing about, in order of how invasive they are. +
+ +--network=host)
+ Running the tracker container with --network=host bypasses Docker's bridge
+ and DNAT layer entirely. The tracker binds directly to the host network namespace, so no
+ NAT rewrite happens and no conntrack entry is created for incoming UDP packets.
+
+ This is what many high-volume public trackers do. Trade-offs: you lose Docker's network
+ isolation between containers, port mappings (-p host:container) are ignored,
+ and the container can collide with any other process listening on the same port on the
+ host.
+
NOTRACK on the Tracker Port
+ If you want to keep bridge networking for isolation, you can tell the kernel to skip
+ connection tracking for traffic on the tracker port using a rule in the
+ raw table. Modern Ubuntu / Debian uses iptables-nft under the
+ hood, so the cleanest way to express these rules is directly in nftables. Add
+ the following to /etc/nftables.conf:
+
Apply and persist across reboots:
+ +For comparison, the equivalent classic iptables form is:
+ With NOTRACK, packets bypass conntrack and the table never grows from tracker
+ traffic. The catch is significant: NAT requires conntrack, so once you
+ stop tracking these packets, Docker's automatic DNAT for the published port no longer
+ works.
+
nftables rules above, confirmed they were active (conntrack -S
+ showed early_drop = 0), and immediately UDP announces from
+ newTrackon and from our own
+ tracker_checker client started timing out. HTTP kept working. Switching the
+ tracker container to network_mode: host (per
+ torrust/torrust-demo#27)
+ did not fix it either, and we eventually had to
+ restore a server backup. A
+ secondary problem we observed: even with port-level NOTRACK, internal Docker
+ traffic to the tracker (statsd on 8125, healthchecks, the index calling the tracker over
+ 127.0.0.1) was still being tracked because those flows go through the
+ loopback / bridge interfaces, not through the public DNAT path.
+
+ The takeaway is that NOTRACK is most useful with macvlan or with a bare-metal
+ install that does not rely on Docker's DNAT/iptables rules. With host networking, many
+ setups do not need NOTRACK at all. In a typical multi-container Docker Compose
+ setup it is fragile and hard to get right.
+
nftables route,
+ run sudo systemctl enable nftables. We hit a case where the rules in
+ /etc/nftables.conf were syntactically valid and present on disk, but
+ nft list ruleset came back empty after a reboot because the
+ nftables service was not enabled.
+ macvlan Network Driver+ The macvlan driver + gives the container its own MAC address and IP on the physical LAN. Packets reach the container + without NAT, so no conntrack entries are created on the host for tracker traffic. This preserves + container isolation but requires more involved network setup (a parent interface in promiscuous + mode, an IP plan, and a host that is allowed to claim multiple MACs — which rules out most cloud + providers that filter on the upstream switch). +
+ +--network=host is usually the simplest and
+ most efficient choice.
+
+ After applying the fix, use these commands to confirm that the table is no longer
+ exhausting. The conntrack CLI is not installed by default on most distributions;
+ install it first:
+
+ The conntrack -S output includes an early_drop counter per CPU. A
+ non-zero value means the kernel had to evict entries early to make room — a leading indicator
+ of exhaustion before packets start dropping. If this counter is growing, you need a larger table
+ or shorter timeouts.
+
+ On the first Torrust demo, we observed 20 million+ early_drop events on CPU 3
+ before the fix. After increasing nf_conntrack_max and adjusting the timeouts, the
+ counter stabilized at zero.
+
nf_conntrack_count / nf_conntrack_max > 0.8. At 80 % fill, entries are
+ still being accepted; at 100 % they are being dropped. Catching it at 80 % gives you time
+ to react without customer-facing impact.
+
+ This is not unique to Torrust. The
+ ftorrent/open README
+ — a comprehensive guide to running the
+ Aquatic tracker in Docker — covers
+ the same problem in its "Kernel tuning for bridge networking" section. That guide
+ documents the same
+ nf_conntrack_max, nf_conntrack_udp_timeout, and
+ nf_conntrack_udp_timeout_stream fixes, and extends them with two additional
+ parameters: net.core.rmem_max / rmem_default to size UDP socket
+ receive buffers, and net.core.netdev_max_backlog to prevent softirq drops
+ when Docker's veth pair adds per-packet overhead. It also covers the same
+ reboot-persistence trap (pre-loading the nf_conntrack module) and provides matching
+ monitoring commands.
+
+ Any UDP service that receives sustained traffic through Docker bridge networking and + Docker's DNAT layer is susceptible. BitTorrent trackers happen to be a high-frequency case + because every peer re-announces periodically, generating a constant stream of short + request–response exchanges. +
+ ++ The resources below independently document the same conntrack problem and cover related + topics for anyone running a public tracker with Docker. +
+nf_conntrack: table full, dropping packet and the initial fix.
+ nftables NOTRACK rules (with and without
+ --network=host), and the localhost-tracking gotcha that affects
+ multi-container Docker setups. Closely related to
+ torrust/torrust-demo#27
+ (Docker network configuration) and
+ torrust/torrust-demo#78 (the
+ backup restore that followed).
+ nf_conntrack_* sysctl parameter,
+ including the default values for nf_conntrack_udp_timeout (30 s),
+ nf_conntrack_udp_timeout_stream (120 s), and
+ nf_conntrack_max.
+ DOCKER nat table for port-mapping) and notes that packets in
+ the DOCKER-USER chain have already been DNAT-rewritten — confirming why the
+ conntrack extension is required to match original IP/port.
+ dmesg and /proc/sys/net/netfilter/.
+ /etc/modules-load.d/conntrack.conf, the sysctl settings will not survive a
+ reboot.
+ nf_conntrack_max and the bucket count (hashsize) are
+ independent. Raising one without the other turns O(1) lookups into O(n) chain walks.
+ --network=host, NOTRACK rules, and the macvlan driver
+ all remove conntrack from the path entirely. Sysctl tuning is the right call when you need
+ bridge networking; otherwise it is treating a symptom.
+ NOTRACK is harder than it looks in a multi-container Docker setup.
+
+ A port-level rule does not catch flows that traverse loopback or the Docker bridge (statsd,
+ healthchecks, container-to-container traffic), and disabling tracking on a NAT-published port
+ breaks Docker's DNAT. We tried it twice on the DigitalOcean demo and reverted both times —
+ see
+ torrust/torrust-demo#72.
+