Guide

A file descriptor (fd) is a small integer that stands for an open file, socket or pipe. Your code never holds the file itself. It holds a number, and the kernel looks that number up in a per-process table to find the real object. Every network connection your service accepts uses one.

Concept

System call: a request your program makes to the kernel to do something only the kernel may do, such as open a file or send a packet. Opening a file, open(), is a system call that returns an fd.

The big idea

Unix calls this "everything is a file". The useful part is that the same read() and write() calls work on very different things. Each row below is created by a different call, but all of them are accessed through an fd.

ObjectCreated by
Regular fileopen()
TCP or UDP socketsocket(), accept()
Pipepipe()
Event notificationepoll_create(), eventfd(), timerfd_create()
Directoryopen() with O_DIRECTORY

Every process starts with three fds: 0 (stdin), 1 (stdout) and 2 (stderr). This is why ls > out.txt works. The shell opens out.txt, gets an fd, and points fd 1 at it before the program starts.

Walking through the limit

Each process has a cap on open fds, set by ulimit -n. The default soft limit is often 1024. Here is a server that holds 1,000 client connections.

client connections     = 1,000
stdin, stdout, stderr  =     3
                         -----
used                   = 1,003
soft limit             = 1,024
room left for logs and database sockets = 21

Only 21 fds are left for everything else. A few more connections, or a bigger database pool, and accept() fails with EMFILE: Too many open files. The fix depends on the cause. Check the limit and the count:

Bash
ulimit -n                              # soft limit for this shell
cat /proc/<pid>/limits | grep "open files"
ls /proc/<pid>/fd | wc -l              # how many it has open right now

Two situations look the same in the logs:

  1. The limit is too low for real load. Raise it in your service manager, such as LimitNOFILE= in systemd. Values like 65,536 are normal for servers.
  2. You are leaking fds. The count in /proc/<pid>/fd climbs forever. Raising the limit only delays the outage.

Tip

Graph open fds per process next to the limit. Prometheus client libraries export process_open_fds and process_max_fds. A line that only goes up is a leak.

Common mistakes

  • Raising the limit to fix a leak. A bigger limit only moves the outage later. Check whether the count keeps climbing.
  • Opening files in a loop without closing them. Use with in Python or defer in Go so the fd closes on every path.
  • Forgetting that sockets count. HTTP client response bodies that are never closed hold their connections open.

Check yourself

  1. What does the integer 5 in a process mean? It is an index into the process's fd table. Entry 5 points to a kernel object, such as a file or socket.
  2. The fd count climbs forever, even after you double the limit. What is likely? A leak. Find the code that opens without closing.

Key takeaways

  • An fd is an integer handle to a kernel object: a file, socket, pipe or event source.
  • Stdin, stdout and stderr are fds 0, 1 and 2. Redirection just changes what those point to.
  • "Too many open files" is either a low limit or a leak. Graph the count to tell which.

Further learning

Next, read processes and threads for the wider model. The /proc directory is the fastest way to see a live process's fd table, so run ls -l /proc/<pid>/fd on something you own.