How nf_conntrack Overflow Causes Intermittent UDP Tracker Downtime with Docker
A subtle Linux kernel resource exhaustion silently drops UDP packets when running a BitTorrent tracker behind Docker bridge networking. No application error, no socket counter — just intermittent timeouts and a self-recovery cycle. Here is how to diagnose it, fix it, and make sure the fix survives a reboot.
Introduction
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.
Our Experience: Repeated Incidents Across Two Demos
First Demo — DigitalOcean (2024–2025)
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.
New Tracker Demo — Hetzner (2026)
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).The Symptom
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:
- UDP availability drops intermittently to 60–90 % while the HTTP tracker stays healthy.
- Outages are self-recovering — they resolve without any operator intervention, typically within seconds to a few minutes.
- You cannot reproduce the problem by sending a single announce manually; it only appears under sustained load.
- There is nothing relevant in the tracker application log, the Docker logs, or
netstat/sssocket counters. - Restarting the tracker or Docker has no lasting effect — the problem returns once load resumes.
- Upgrading the server to a larger instance (more CPU, more RAM) makes things worse because the tracker can now handle more requests per second, which fills the conntrack table faster.
Why It's Hard to Diagnose
The standard places you look for dropped packets do not show this problem:
- Application log: the tracker process never sees the dropped packet. The kernel drops it before it reaches the socket.
- Socket receive-buffer drops:
ss -u -sandnetstat -sushow socket-level drops, not kernel-level conntrack drops. They will not increment. - Firewall logs:
iptables/ufwlog rules fire on packets that reach the firewall. A packet dropped by the conntrack subsystem before the firewall never appears in those logs. - Docker logs: Docker has no visibility into kernel packet drops.
The primary evidence is in dmesg and conntrack counters in /proc/sys/net/netfilter/.
# Look for the telltale message:
dmesg | grep -i conntrack
# nf_conntrack: table full, dropping packet
# Check current fill level:
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
# If count equals max, the table is full right now.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.
The Mechanism: Docker DNAT and Conntrack
How Docker Publishes UDP Ports
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.
How Entries Accumulate Under UDP Tracker Load
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:
- One-way (unreplied) UDP: default timeout is 30 seconds.
- Bidirectional (replied) UDP: default timeout is 120 seconds.
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 Calculation
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:
- Maximum safe request rate = 262 144 ÷ 120 ≈ 2 186 requests/s
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:
- 262 144 ÷ 15 ≈ 17 476 requests/s at the default table size
Combining a larger table with a shorter timeout gives significant headroom even on a busy public tracker.
The Fix: Three Kernel Parameters
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):
# Raise the table ceiling.
# Default 65536–262144 is too small under tracker load.
net.netfilter.nf_conntrack_max = 1048576
# Reduce UDP stream timeout.
# Default 120 s; a tracker announce round-trip completes in milliseconds.
net.netfilter.nf_conntrack_udp_timeout_stream = 15
# Reduce one-way UDP timeout.
# Default 30 s.
net.netfilter.nf_conntrack_udp_timeout = 10Apply the settings immediately without rebooting:
sudo sysctl --system
# or apply only this file:
sudo sysctl -p /etc/sysctl.d/99-conntrack.confVerify that the new values are active:
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_udp_timeout_stream
sysctl net.netfilter.nf_conntrack_udp_timeoutnf_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.Don't Forget the Hash Table
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.
# Runtime (sysctl): 262144 buckets pairs well with nf_conntrack_max = 1048576
sudo sysctl -w net.netfilter.nf_conntrack_buckets=262144
# Persistent (sysctl)
echo 'net.netfilter.nf_conntrack_buckets = 262144' | sudo tee /etc/sysctl.d/98-conntrack-buckets.conf
# Optional early-boot module option (equivalent bucket size)
echo 'options nf_conntrack hashsize=262144' | sudo tee /etc/modprobe.d/nf_conntrack.conf
# Verify
sysctl net.netfilter.nf_conntrack_buckets
cat /sys/module/nf_conntrack/parameters/hashsizeReduced Timeouts Are Global
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.
The Reboot Persistence Trap
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:
echo "nf_conntrack" | sudo tee /etc/modules-load.d/conntrack.confWith 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:
lsmod | grep nf_conntrack
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_udp_timeout_streamAlternative Approaches: Avoid the Problem Entirely
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.
1. Host Networking (--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.
# docker-compose.yml — host networking for the UDP tracker
services:
tracker:
image: torrust/tracker:latest
network_mode: host
# 'ports:' are ignored when network_mode: host
# The tracker binds to 0.0.0.0:6969 on the host directly.2. 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:
# /etc/nftables.conf — disable conntrack for the UDP tracker ports
table inet raw {
chain prerouting {
type filter hook prerouting priority raw;
udp dport { 6868, 6969 } notrack
}
chain output {
type filter hook output priority raw;
udp sport { 6868, 6969 } notrack
}
}Apply and persist across reboots:
sudo systemctl enable nftables # crucial — without this the rules are not loaded at boot
sudo systemctl restart nftables
sudo nft list ruleset | grep notrackFor comparison, the equivalent classic iptables form is:
sudo iptables -t raw -A PREROUTING -p udp --dport 6969 -j NOTRACK
sudo iptables -t raw -A OUTPUT -p udp --sport 6969 -j NOTRACK
# IPv6
sudo ip6tables -t raw -A PREROUTING -p udp --dport 6969 -j NOTRACK
sudo ip6tables -t raw -A OUTPUT -p udp --sport 6969 -j NOTRACKWith 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.3. 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.Monitoring and Verification
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:
# Debian / Ubuntu
sudo apt-get install -y conntrack
# RHEL / Fedora / Rocky / Alma
sudo dnf install -y conntrack-tools# Current fill level (watch for count approaching max)
watch -n5 'echo "count: $(cat /proc/sys/net/netfilter/nf_conntrack_count) / max: $(cat /proc/sys/net/netfilter/nf_conntrack_max)"'
# Cross-check: nf_conntrack_count should match the number of entries listed by the conntrack tool
sudo conntrack -L 2>/dev/null | wc -l
cat /proc/sys/net/netfilter/nf_conntrack_count
# Drop messages since boot
dmesg | grep -c "table full"
# Conntrack statistics per CPU (early_drop column indicates table pressure)
sudo conntrack -S
# One-liner for the drop count across all CPUs
sudo conntrack -S | awk '{for (i=1;i<=NF;i++) if ($i ~ /^early_drop=/) { split($i,a,"="); sum += a[2] } } END {print "total early_drop:", sum+0}'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.Independent Documentation
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.
Further Reading
The resources below independently document the same conntrack problem and cover related topics for anyone running a public tracker with Docker.
- Running Aquatic in Docker: A Complete Guide to Public BitTorrent and WebTorrent Trackers — A detailed guide to deploying the Aquatic tracker (a Rust implementation of all three BitTorrent tracker protocols) in hardened Docker containers. Covers conntrack tuning, UDP socket buffers, NIC backlog, Docker bridge networking security, container hardening with dropped capabilities and custom seccomp profiles, IPv6 dual-stack, and reverse proxy setup. The "Kernel tuning for bridge networking" section is directly relevant to this post.
- torrust/torrust-demo#26 — The GitHub issue tracking our first encounter with this problem on the DigitalOcean demo.
Includes the kernel journal output showing
nf_conntrack: table full, dropping packetand the initial fix. - torrust/torrust-demo#72 — The follow-up issue from June 2025 documenting the second occurrence on the same DigitalOcean
droplet, the failed attempt to disable conntrack with
nftablesNOTRACKrules (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). - torrust/torrust-tracker-demo#21 — The issue tracking the second occurrence on the Hetzner tracker demo, along with PR #22 which added the sysctl settings and the conntrack module pre-load to the deployer.
Related Posts on This Blog
- The New Torrust Tracker Demo Is Live — Introduces the Hetzner tracker demo that was affected by the conntrack overflow described in this post. Covers the deployment architecture, observability stack, and goals for the demo.
- Deploying the Torrust Tracker Demo with the Torrust Tracker Deployer — Step-by-step walkthrough of deploying the Hetzner tracker demo using the Torrust Tracker Deployer, including the sysctl and kernel module changes introduced to fix this conntrack issue.
- How to Run a UDP Tracker Behind a Floating IP on Ubuntu — A companion networking guide covering policy routing, Docker IPv6 networking, and SNAT for correct UDP reply paths — closely related problems to the one described here.
- Submitting Trackers to newTrackon — How to register your tracker with newTrackon, the uptime-monitoring service used throughout this post to detect and confirm the conntrack problem.
- Visualize Tracker Metrics with Prometheus and Grafana — How to set up the observability stack used to monitor the Torrust Tracker. Pairs well with the monitoring commands in this post for a complete view of tracker health.
- Containerizing Rust Applications — Best practices for building and running Torrust services in Docker, including the bridge networking configuration that makes conntrack tuning necessary.
Official Documentation
- Linux kernel: Netfilter Conntrack Sysfs variables — The authoritative reference for every
nf_conntrack_*sysctl parameter, including the default values fornf_conntrack_udp_timeout(30 s),nf_conntrack_udp_timeout_stream(120 s), andnf_conntrack_max. - Docker Engine: Port publishing and mapping — Explains how Docker uses NAT, PAT, and masquerading to forward traffic to published container ports, and the role of iptables firewall rules in that process.
- Docker Engine: Docker with iptables — Documents the custom iptables chains Docker creates (including the
DOCKERnattable for port-mapping) and notes that packets in theDOCKER-USERchain have already been DNAT-rewritten — confirming why theconntrackextension is required to match original IP/port. - Docker Engine: Packet filtering and firewalls — Overview of Docker's firewall rule model for bridge networks, including masquerading and the interaction with external firewall tools.
- Docker Engine: Bridge network driver — Covers how Docker's default bridge network works, including IP masquerading and port publishing to host addresses.
Lessons
- The application log is not enough. For kernel-level drops, check
dmesgand/proc/sys/net/netfilter/. - A larger server can make conntrack exhaustion worse, not better. More throughput fills the table faster if the table size is unchanged.
- Always pre-load the module. Without
/etc/modules-load.d/conntrack.conf, the sysctl settings will not survive a reboot. - This affects any UDP service behind Docker bridge networking at non-trivial request rates — not just BitTorrent trackers. DNS resolvers, game servers, VoIP services, and QUIC-based applications are equally vulnerable.
- Reducing UDP timeouts is safe for request–response protocols. A BitTorrent announce completes in milliseconds. The default 120-second stream timeout exists for stateful protocols; for stateless UDP services, shorter timeouts are appropriate and dramatically increase effective table capacity.
- Monitor conntrack fill level proactively. An alert at 80 % gives you time to respond before packets start dropping.
- Resize the hash table when you raise the ceiling.
nf_conntrack_maxand the bucket count (hashsize) are independent. Raising one without the other turns O(1) lookups into O(n) chain walks. - Consider eliminating the problem instead of tuning around it.
--network=host,NOTRACKrules, and themacvlandriver all remove conntrack from the path entirely. Sysctl tuning is the right call when you need bridge networking; otherwise it is treating a symptom. NOTRACKis 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.