ProductionAdvanced

Diagnosing EMFILE

“A service starts logging `EMFILE: too many open files` after about a day of uptime. How do you approach it? Is raising the limit the fix?”

What this tests

  • Distinguishing legitimate load from a leak by trending the count
  • Classifying descriptors: sockets vs files vs pipes vs anon inodes
  • Reading CLOSE_WAIT as an application bug
  • The limit chain and the order of operations: find the leak first, raise the limit second

Answers by level

Read the beginner answer first and notice what is missing.

EMFILE means the per-process descriptor table is full — the soft RLIMIT_NOFILE, often 1,024 by default and raised by the service manager to 65,536 (label: Linux/Unix; ENFILE is the system-wide table). The first question is not "how high" but "what is in the table": ls /proc/<pid>/fd | wc -l sampled every few minutes tells you whether the count tracks load (bounded, fine) or grows monotonically since start (a leak) (File Descriptors).

Then classify. lsof -p <pid> (or ls -l /proc/<pid>/fd) shows each descriptor’s type: regular files with a path, sockets with peers, pipes, anon_inode entries for epoll/eventfd/timerfd. Hundreds of sockets to the same upstream is one story; hundreds of the same log path is another; thousands of pipes means child processes. For sockets, ss -tanp | grep <pid> gives the state: many `CLOSE_WAIT` means the peer closed and the application never called close() — a leak in the client code, not a kernel problem. TIME_WAIT sockets, by contrast, hold no descriptor and are the kernel’s business (The Socket: A Descriptor With Two Kernel Buffers Behind It).

Common causes: an HTTP client or database connection created per request and never closed or pooled; a response body not consumed/closed (resp.Body.Close() in Go, unconsumed streams in Node.js); a file opened in a code path whose error branch skips close; a keepAlive agent per request whose idle sockets outlive any reference; child processes inheriting descriptors. Fix the ownership — pooling, try/finally, RAII, using — and add a descriptor-count metric so the next leak is a graph, not an outage (The OS Debugging Playbook: Four Symptoms, Fourteen Causes).

Raising the limit is the *second* step, and legitimate: a server holding 20,000 connections needs a table that size, and the soft limit should be set at the hard limit (LimitNOFILE= in systemd, worker_rlimit_nofile in nginx). But raising it for a leak only moves the failure to a bigger, later, more confusing incident.

Green flags · Red flags

Strong green flag · Adds a descriptor-count metric and a CLOSE_WAIT alert as part of the fix.
Green flags
  • Trends the descriptor count before touching the limit
  • Classifies descriptors by type with lsof or /proc
  • Reads CLOSE_WAIT as "the app never closed" and TIME_WAIT as not a descriptor
  • Names concrete leak patterns in real client libraries
  • Raises the limit second and explains when it is legitimate
Red flags
  • Only raises the limit
  • Confuses TIME_WAIT with a leak
  • Does not know where to see what the descriptors are

Follow-up questions

F1
lsof shows 900 sockets to one upstream in CLOSE_WAIT. Whose bug?
F2
When is raising the limit the right primary fix?
F3
What does EMFILE do to a server’s accept loop?

Scenario

A Node.js worker reads thousands of small files per job with Promise.all and throws EMFILE only in production, not in tests. Explain why, and give a fix that is not "raise ulimit".

Learn this topic