Linux Under Load: Epoll, File Descriptors, OOM, TCP Queues, and VFS
Connect kernel readiness APIs, resource limits, cgroups, socket queues, page cache, and inode behavior to production symptoms.
On this page
1. Many sockets and epoll
select() and traditional poll() require repeatedly presenting a set of descriptors to inspect; select() also has descriptor-set limits on many systems. epoll maintains interest and ready lists in the kernel and reports descriptors whose state changed, which scales better for large mostly-idle connection sets. It does not make application work free: edge-triggered use requires draining until EAGAIN, and every ready socket still needs correct lifecycle handling.
An application can hit Too many open files from a per-process RLIMIT_NOFILE, system-wide file table pressure, or a leak. Find the owner with lsof -p <pid> and inspect /proc/<pid>/fd; lsof -i narrows to network sockets. Check ulimit -n in the service's actual launch environment and systemd/container limits. A temporary soft-limit increase must stay under the hard limit and should be approved; raising limits without fixing an FD leak only delays failure.
2. Trace syscalls and network counters
strace -f -e trace=network -p <pid> can reveal repeated failed connects, bind errors, or socket lifecycle behavior, but tracing can add overhead and expose addresses. Use a short, authorized capture. For interface counters, compare ip -s link over time; ethtool -S eth0 exposes driver counters, and ethtool -g eth0 reports ring parameters when supported. Drops may arise in the driver, kernel backlog, qdisc, or application—not all are fixed by increasing a ring buffer.
Backlogs involve several queues: listen backlog, somaxconn, SYN handling, and application accept rate. Inspect them with socket state and kernel counters before changing sysctls. For TCP keepalive, distinguish dead-peer detection from application request deadlines.
3. TIME_WAIT and port pressure
TIME_WAIT is part of TCP's safe connection teardown and prevents delayed packets from a prior connection being mistaken for a new one. A high count alone is not proof of a fault. Diagnose source-port exhaustion by checking connection tuple reuse, NAT limits, ephemeral port range, connection pooling, and destination fan-out. net.ipv4.tcp_tw_reuse behavior is kernel-version and context dependent; do not use it as a universal fix. Prefer connection reuse and correct client lifecycle.
4. Memory, cgroups, and the OOM killer
Linux uses page cache as reclaimable memory, so low “free” memory alone is not evidence of pressure. Inspect memory available, cgroup v2 memory.current, memory.max, memory.events, and process RSS. The OOM killer selects a victim based on system context and oom-score adjustments; a container may hit its cgroup limit while the host still has spare memory.
For a JVM in a container, ensure -Xmx leaves room under memory.max for metaspace, code cache, native buffers, thread stacks, and runtime overhead. If -Xmx consumes nearly the whole cgroup limit, the kernel may kill the process before Java can raise a heap error. Use container-aware JVM support and leave measured headroom.
5. Page cache, inodes, and truncation semantics
The page cache buffers file data and usually helps performance; dropping caches is not a routine remedy for “Linux used memory.” A file has a pathname entry and an inode. > app.log opens and truncates the same inode to zero while an existing writer may continue writing through its descriptor. rm app.log removes the directory entry, but an open descriptor keeps the inode and blocks alive until the last process closes it. This is why disk usage can remain high after deletion.
Interview and troubleshooting drills
Interview: Explain why epoll handles many mostly-idle connections more efficiently than repeatedly scanning all descriptors, and what edge-triggered code must do after readiness notification.
Troubleshooting: An API reports EMFILE. Identify the process/thread, inspect /proc/<pid>/fd and lsof, compare soft/hard limits in the service unit, find the unclosed descriptor path, then apply a temporary scoped limit change only if required to restore service.
Challenge: A JVM has -Xmx close to its cgroup v2 memory maximum. Explain why native stacks, direct buffers, metaspace, and page tables can push memory.current over the limit even when Java heap usage is below -Xmx; use memory events and GC logs to distinguish kernel OOM from Java heap exhaustion.