perf top on an affected edge node, the user-space resolver binaries were nowhere near the top of CPU accounting. Instead, kernel cycles were entirely consumed by tcp_collapse and tcp_prune_queue inside softirq context.
The Hidden Mechanics of Socket Memory Accounting
sk_buff. The memory footprint of this structure is not just the raw byte length of the incoming payload; it is represented by sk_buff->truesize, which includes the data buffer, alignment padding, and the sk_buff control structure itself. A 64-byte TCP packet can easily consume upwards of 1,500 to 2,048 bytes of actual slab and page memory.
Under legacy cgroups v1, network socket memory was often ignored or segregated into the optional net_prio and memory.kmem.tcp.limit_in_bytes controllers, which most production container runtimes left disabled. However, cgroups v2 fundamentally changed this paradigm. Under unified hierarchy accounting, network socket buffers (memory.sock) are charged directly against the container's primary memory.current and evaluated against memory.max.
TEXT
Container memory.current
+-- Page Cache (File I/O)
+-- Anonymous Memory (Resolver Heap, TLS Sessions)
+-- memory.sock (sk_buff allocations via tcp_try_rmem_schedule)
^
+-- Must stay below cgroup memory.maxtcp_try_rmem_schedule() to verify that the socket's receive buffer can expand. If this allocation pushes the container over memory.max, the kernel refuses to grow the socket buffer. Instead of immediately dropping the packet or invoking the OOM killer—which only triggers when user-space page allocations fail under direct reclaim—the TCP stack attempts to survive by compacting existing in-flight buffers.
Inside tcp_prune_queue and tcp_collapse
tcp_try_rmem_schedule() returns false, the socket enters emergency queue pruning via tcp_prune_queue(). The kernel inspects the socket's out-of-order queue and receive queue to recover memory without dropping data on the floor.
First, out-of-order packets are discarded to free immediate space. If memory pressure persists, the kernel executes tcp_collapse(). This function traverses the socket's linear receive queue, allocates a fresh, contiguous buffer, and copies the payloads from dozens of fragmented sk_buffs into a single contiguous chunk. By consolidating these fragments, the kernel discards the redundant sk_buff overhead headers, shrinking the truesize back toward the actual payload size.
BASH
# Check if your systems are silently burning CPU cycles collapsing buffers
nstat -az TcpExtTCPRcvCollapsed TcpExtTCPPruneDroptcp_collapse() under high concurrency introduces crippling latencies. The compaction loop runs with the socket lock held. If thousands of concurrent DoT connections cross their cgroup memory allocations at once, the softirq handler loops through linked lists of thousands of packets, executing memory copies (memcpy) inside interrupt and system call contexts.
Because the socket lock is held during this compaction, any user-space call to epoll_wait or read on that file descriptor stalls waiting for the lock. The user-space event loop misses subsequent epoll wakeups, incoming TCP window advertisements drop to zero (win 0), and the entire resolver pipeline stalls.
Why DNS-over-TLS Amplifies the Problem
sk_buff allocations simultaneously.
Second, default Linux sysctl parameters are designed for bulk throughput on modern high-bandwidth servers, not containerized micro-transactions. Consider the default TCP receive memory boundaries:
TEXT
net.ipv4.tcp_rmem = 4096 131072 6291456memory.max hosting 15,000 active DoT client connections, the mathematical upper bound for socket allocations far exceeds the container limit. When an edge traffic spike lands, the sockets naturally attempt to scale their receive windows, instantly slamming into memory.max.
Instead of gracefully shedding load via standard TCP drops, the cgroup forced the entire pool of sockets into continuous, concurrent tcp_collapse passes. The system entered an unrecoverable thrashing state: softirq workers held socket locks to compress buffers, user-space was locked out from draining the sockets, and clients retransmitted their packets, worsening the queue backlogs.
Hardening the Socket and Cgroup Boundary
1. Clamp sysctl tcp_rmem for Containerized Handshake Gateways
tcp_rmem, you prevent individual sockets from inflating their sk_buff footprint:
BASH
# /etc/sysctl.d/99-dot-performance.conf
net.ipv4.tcp_rmem = 4096 16384 262144
net.ipv4.tcp_wmem = 4096 16384 2621442. Configure Cgroup memory.high as a Pressure Bleed
memory.sock to hit memory.max directly. In cgroups v2, memory.high acts as an asynchronous throttling throttle rather than a hard kill boundary. When usage crosses memory.high, the kernel proactively slows down allocating processes by injecting small delays in the return path to user space, giving background direct reclaim and memory management threads time to breathe before tcp_prune_queue is forced into synchronous execution.
Ensure that memory.high is set to approximately 80% to 85% of memory.max. This leaves an essential buffer cushion specifically for network packet ingress during transient spikes.
3. Application-Level Socket Buffer Pinning
SO_RCVBUF via setsockopt forces the kernel to disable autotuning and hard-limits the receive queue allocation per connection, completely neutralizing the container-wide tcp_collapse avalanche.
Operating edge TLS resolvers inside cgroups v2 requires recognizing that network buffers are no longer isolated kernel freebies. When memory limits are drawn, the networking stack must be budgeted with the same defensive boundaries applied to the heap.