"Too many open files" systematisch debuggen
AI generated
$
/etc
Linux · Troubleshooting · File Descriptor · PHP-FPM
"Too many open files" systematisch debuggen
Wie man File-Descriptor-Lecks findet, bevor der nächste Prozess abstürzt

Der Fehler EMFILE, gemeldet als "Too many open files", ist selten ein Zufall und fast nie eine reine Kapazitätsfrage. Meistens öffnet eine Anwendung Verbindungen, Dateien oder Sockets und schließt sie nie wieder. Dieser Leitfaden zeigt, wie man mit lsof, dem Verzeichnis /proc/pid/fd und gezieltem Monitoring herausfindet, welcher Codepfad Dateideskriptoren verliert und wie man die Grenzwerte richtig konfiguriert, ohne das eigentliche Leck zu übertünchen.

17 Min. Lesezeit lsof · ulimit · /proc/pid/fd · EMFILE Linux · Troubleshooting · PHP-FPM · Nginx

1. Was hinter "Too many open files" wirklich steckt

Die Fehlermeldung "Too many open files" entspricht dem Errno-Code EMFILE oder, seltener, ENFILE. EMFILE bedeutet, dass ein einzelner Prozess sein eigenes Limit an offenen Dateideskriptoren erreicht hat, während ENFILE bedeutet, dass die systemweite Obergrenze aller Prozesse zusammen erschöpft ist. Unter Linux zählt praktisch alles als Dateideskriptor: reguläre Dateien, Netzwerk-Sockets, Pipes, Unix-Domain-Sockets und sogar epoll-Instanzen. Ein Webserver, der viele gleichzeitige Verbindungen hält, oder ein PHP-Prozess, der Datenbankverbindungen aufbaut, kann daher schnell an ein Limit stoßen, das ursprünglich für klassische Dateizugriffe gedacht war.

Der entscheidende Denkfehler bei diesem Fehler ist, ihn ausschließlich als Kapazitätsproblem zu behandeln und das Limit einfach zu erhöhen. In den meisten produktiven Fällen liegt die eigentliche Ursache in einem File-Descriptor-Leck: Ein Codepfad öffnet eine Datei, einen Socket oder eine Datenbankverbindung, vergisst aber, sie bei einem Fehlerpfad oder einer frühen Rückkehr wieder zu schließen. Ein höheres Limit verzögert dann nur den unvermeidlichen nächsten Absturz, statt das eigentliche Leck zu beheben. Genau wie bei Speicherlecks braucht "Too many open files" deshalb eine systematische Untersuchung, bevor man an den Konfigurationswerten dreht.

2. Limits verstehen: ulimit, /proc/sys/fs/file-max und Systemd

Bevor man ein Leck sucht, muss man wissen, welches Limit überhaupt greift. ulimit -n in einer interaktiven Shell zeigt das Soft-Limit für den aktuellen Prozessbaum, während ulimit -Hn das Hard-Limit anzeigt, das ein unprivilegierter Prozess selbst nicht überschreiten kann. Für Dienste, die über systemd gestartet werden, gilt aber nicht das Shell-Limit, sondern der Wert aus LimitNOFILE in der jeweiligen Unit-Datei oder, falls dort nicht gesetzt, der systemweite Standard aus /etc/systemd/system.conf. Ein häufiger Fehler ist, ulimit -n 65536 in /etc/security/limits.conf zu setzen und sich zu wundern, dass ein über systemd gestarteter Dienst trotzdem an EMFILE scheitert, weil PAM-Limits und Systemd-Limits zwei getrennte Mechanismen sind.

Die systemweite Obergrenze /proc/sys/fs/file-max begrenzt zusätzlich, wie viele Dateideskriptoren insgesamt über alle Prozesse hinweg offen sein dürfen. Auf modernen Servern mit ausreichend Arbeitsspeicher ist dieser Wert selten der limitierende Faktor, sollte aber trotzdem mit cat /proc/sys/fs/file-nr überprüft werden, das die aktuell genutzten, zugeteilten und maximalen Deskriptoren anzeigt. Erst wenn beide Ebenen, das Pro-Prozess-Limit und das systemweite Limit, bekannt sind, lässt sich beurteilen, ob ein Fehler tatsächlich an einer zu niedrigen Konfiguration liegt oder an einem echten Leck.


# Soft and hard limit for the current shell session
ulimit -n
ulimit -Hn

# Effective limit for a running process, read directly from /proc
cat /proc/$(pgrep -f php-fpm | head -1)/limits | grep "Max open files"

# System-wide file descriptor usage: allocated, free, maximum
cat /proc/sys/fs/file-nr

# Limit currently configured for a systemd-managed service
systemctl show php8.4-fpm.service --property=LimitNOFILE

3. Offene Dateideskriptoren mit lsof zählen und kategorisieren

lsof (list open files) ist das zentrale Werkzeug, um zu sehen, was ein Prozess tatsächlich offen hält. lsof -p $PID listet jeden einzelnen Deskriptor mit Typ, Größe und Ziel auf, während lsof -p $PID | wc -l eine schnelle Gesamtzahl liefert, die man gegen das konfigurierte Limit vergleichen kann. Entscheidend für die Diagnose eines File-Descriptor-Lecks ist aber nicht die reine Zahl, sondern die Verteilung nach Typ: Viele Einträge vom Typ REG (reguläre Datei) deuten auf nicht geschlossene Dateihandles hin, viele Einträge vom Typ IPv4/IPv6 auf nicht geschlossene Netzwerkverbindungen, etwa zur Datenbank oder zu einem externen API.

Besonders aufschlussreich ist die Spalte mit dem Dateipfad selbst. Tauchen dort auffällig viele identische Pfade auf, etwa immer dieselbe Log-Datei oder derselbe Socket zu Redis, deutet das auf einen Codepfad hin, der bei jedem Aufruf eine neue Verbindung öffnet, statt eine bestehende wiederzuverwenden oder korrekt zu schließen. Mit lsof -p $PID | awk '{print $5}' | sort | uniq -c | sort -rn lässt sich diese Verteilung in Sekunden erzeugen und liefert oft schon die entscheidende Spur, bevor überhaupt ein Debugger oder strace zum Einsatz kommt.


# Total number of open file descriptors for one process
lsof -p 4823 | wc -l

# Breakdown by file descriptor type (REG, IPv4, sock, pipe, ...)
lsof -p 4823 | awk '{print $5}' | tail -n +2 | sort | uniq -c | sort -rn

# Which specific paths/sockets appear suspiciously often?
lsof -p 4823 | awk '{print $9}' | tail -n +2 | sort | uniq -c | sort -rn | head -10

# Aggregate open FD count across all PHP-FPM worker processes
for pid in $(pgrep -f "php-fpm: pool"); do
  echo "$pid: $(lsof -p "$pid" 2>/dev/null | wc -l) fds"
done

4. /proc/[pid]/fd im Detail: Sockets, Pipes und gelöschte Dateien

Wenn lsof nicht installiert werden kann oder soll, liefert das Verzeichnis /proc/[pid]/fd dieselben Informationen ohne Zusatzsoftware. Jeder Eintrag darin ist ein Symlink, dessen Ziel mit readlink ausgelesen werden kann, und ls -la /proc/[pid]/fd | wc -l liefert dieselbe Gesamtzahl wie lsof. Besonders wertvoll für ein File-Descriptor-Leck ist die Erkennung von Einträgen, deren Ziel mit (deleted) markiert ist: Das sind Dateien, die vom Dateisystem bereits gelöscht wurden, aber weil ein Prozess sie noch offen hält, belegt der zugrunde liegende Speicherplatz weiterhin Platz auf der Festplatte. Dieses Muster tritt häufig bei Log-Rotation auf, wenn logrotate eine Datei umbenennt oder löscht, der schreibende Prozess aber nie ein SIGHUP oder Äquivalent erhält, um die Datei neu zu öffnen.

Ein zweites häufiges Muster sind Einträge vom Typ socket:[inode] ohne erkennbaren Pfad. Um herauszufinden, zu welcher Verbindung ein solcher Socket gehört, hilft der Abgleich der Inode-Nummer mit der Ausgabe von ss -tp, das für TCP-Verbindungen die zugehörige Inode und den Prozess anzeigt. Diese Kombination aus /proc/[pid]/fd und ss erlaubt eine vollständige Bordmittel-Diagnose, selbst auf minimalen Containern ohne lsof.


# List all file descriptors with their symlink targets
ls -la /proc/4823/fd

# Find descriptors pointing to files already deleted from disk
# (classic logrotate-without-reopen symptom)
for fd in /proc/4823/fd/*; do
  target=$(readlink "$fd")
  [[ "$target" == *"(deleted)"* ]] && echo "$fd -> $target"
done

# Match a socket inode from /proc/fd back to its TCP connection
ls -la /proc/4823/fd | grep socket
ss -tp | grep "pid=4823"

Wie bei jedem Ressourcenleck beweist eine einzelne Zählung nichts. Ein normaler Webserver kann kurzzeitig hunderte offene Deskriptoren haben, ohne dass ein Problem vorliegt, solange die Zahl nach einem Lastabfall wieder sinkt. Ein File-Descriptor-Leck zeigt sich erst, wenn die Anzahl offener Deskriptoren über Stunden monoton wächst, unabhängig von der aktuellen Anfragerate. Ein einfacher Cron-Job, der die Ausgabe von lsof -p $PID | wc -l jede Minute in eine Logdatei schreibt, liefert innerhalb weniger Stunden eine eindeutige Kurve.

Besonders aufschlussreich ist die Korrelation dieser Kurve mit der Anzahl bearbeiteter Requests aus dem Access-Log oder dem PHP-FPM-Statusendpunkt. Wächst die Zahl offener Deskriptoren proportional zur Anzahl der Requests und übersteigt dabei deutlich die Zahl gleichzeitig aktiver Verbindungen, ist das der eindeutige Beweis für ein Leck: Jeder Request hinterlässt im Schnitt einen zusätzlichen, nie geschlossenen Deskriptor. Ohne diese Korrelation bleibt jede Diskussion über die Ursache reine Spekulation.


#!/usr/bin/env bash
# fd-watch.sh — log open file descriptor count for a PID over time
set -euo pipefail

PID="${1:?Usage: fd-watch.sh <pid> <log-file>}"
LOG_FILE="${2:?Usage: fd-watch.sh <pid> <log-file>}"

echo "timestamp,open_fds" > "$LOG_FILE"

while kill -0 "$PID" 2>/dev/null; do
  count=$(ls "/proc/$PID/fd" 2>/dev/null | wc -l)
  echo "$(date -Iseconds),$count" >> "$LOG_FILE"
  sleep 60
done

6. Typische Ursachen in PHP-FPM- und Nginx-Setups

Im PHP-Umfeld entstehen File-Descriptor-Lecks meist durch nicht geschlossene Datenbankverbindungen, insbesondere wenn persistente PDO-Verbindungen (PDO::ATTR_PERSISTENT) falsch verwaltet werden und sich pro Worker-Prozess über die Zeit ansammeln, statt wiederverwendet zu werden. Ein weiterer klassischer Fall sind cURL-Handles, die mit curl_init() geöffnet, aber bei einer Exception vor dem curl_close()-Aufruf nie freigegeben werden. Auch das Schreiben in Log-Dateien über einen einmal geöffneten Filehandle, kombiniert mit Log-Rotation ohne korrektes Reopen-Signal, führt zu wachsendem, weil ungenutztem Speicherplatzverbrauch durch gelöschte, aber offen gehaltene Dateien.

Auf der Nginx-Seite ist der häufigste Auslöser eine zu niedrig konfigurierte worker_rlimit_nofile-Direktive in Kombination mit vielen langlaufenden Upstream-Verbindungen, etwa bei Keepalive zu PHP-FPM oder zu einem Reverse-Proxy-Ziel. Wenn worker_connections in events {} höher konfiguriert ist als worker_rlimit_nofile es erlaubt, meldet Nginx im Fehler-Log genau die Situation, die zu "Too many open files" führt, sobald die tatsächliche Verbindungszahl das interne Limit erreicht. Diese Konfigurationsdiskrepanz ist schnell behoben, sollte aber nicht mit einem echten Leck verwechselt werden.


# nginx.conf — worker_rlimit_nofile must be >= worker_connections
worker_rlimit_nofile: 65535
events:
  worker_connections: 32768
  # If worker_connections exceeds worker_rlimit_nofile, upstream keepalive
  # connections will fail with "too many open files" under sustained load.

7. Mit strace den verantwortlichen Syscall identifizieren

Wenn lsof und /proc/[pid]/fd zeigen, dass ein bestimmter Verbindungstyp wächst, aber unklar bleibt, welcher Codepfad dafür verantwortlich ist, liefert strace -e trace=open,openat,socket,close -p $PID die zeitliche Abfolge aller relevanten Syscalls. Ein Leck zeigt sich hier typischerweise als eine Serie von open()- oder socket()-Aufrufen ohne entsprechende close()-Aufrufe im Verhältnis 1:1. Die Ausgabe lässt sich mit grep -c auf beide Syscall-Typen filtern, um die Differenz über einen Beobachtungszeitraum zu quantifizieren, ohne jede einzelne Zeile manuell durchgehen zu müssen.

Für PHP-Anwendungen liefert zusätzlich strace -f mit dem -f-Flag für Kindprozesse wichtige Details, wenn PHP-FPM Worker über fork() erzeugt. Kombiniert mit einem Zeitstempel (-tt) lässt sich exakt bestimmen, zu welchem Request ein nicht geschlossener Deskriptor gehört, was den Abgleich mit dem Access-Log und damit die Identifikation der auslösenden URL oder Funktion erheblich erleichtert.


# Trace file and socket related syscalls with timestamps
strace -tt -f -e trace=open,openat,socket,close -p 4823 2>&1 | tee /tmp/fdtrace.log

# Count opens vs. closes — a growing gap confirms a leak
grep -cE '^\[pid [0-9]+\] (open|openat|socket)\(' /tmp/fdtrace.log
grep -cE '^\[pid [0-9]+\] close\(' /tmp/fdtrace.log

# Correlate a specific leaked fd number back to its open() call
grep "= 87$" /tmp/fdtrace.log

8. Limits korrekt konfigurieren, ohne das Leck zu verschleiern

Nach der Ursachenanalyse bleibt die Frage, wie man Limits sinnvoll setzt. Die Faustregel: Das Limit sollte großzügig über der erwarteten Spitzenlast liegen, aber niedrig genug, dass ein tatsächliches Leck den Prozess innerhalb überschaubarer Zeit sichtbar an sein Limit stoßen lässt, statt tagelang unbemerkt zu wachsen. Für PHP-FPM setzt man LimitNOFILE in der systemd-Unit oder per php_admin_value[rlimit_files] pro Pool, für Nginx worker_rlimit_nofile passend zu worker_connections.

Ergänzend sollte ein Monitoring-Alarm bei einem Prozentsatz des konfigurierten Limits ausgelöst werden, etwa bei 80 Prozent der offenen Deskriptoren im Verhältnis zum Limit aus /proc/[pid]/limits. Das gibt dem Team Zeit, ein File-Descriptor-Leck zu untersuchen, bevor der Prozess tatsächlich mit EMFILE abstürzt. Wichtig ist, das erhöhte Limit stets als temporäre Absicherung zu behandeln und in einem Ticket zu dokumentieren, dass die eigentliche Ursache im Code noch offen ist, damit das Problem nicht in Vergessenheit gerät.


; /etc/systemd/system/php8.4-fpm.service.d/limits.conf
[Service]
LimitNOFILE=16384

; /etc/php/8.4/fpm/pool.d/www.conf — per-pool override if needed
[www]
rlimit_files = 16384

9. Diagnoseansätze für File-Descriptor-Probleme im Vergleich

Je nach Situation eignen sich unterschiedliche Werkzeuge besser oder schlechter, um ein File-Descriptor-Leck einzugrenzen. Die folgende Übersicht ordnet die vorgestellten Ansätze nach Anwendungsfall.

Werkzeug Stärke Grenze Ohne Zusatzsoftware
lsof Schnelle Übersicht mit Typ und Pfad Muss installiert sein Nein
/proc/[pid]/fd Immer verfügbar, auch in Containern Weniger komfortabel als lsof Ja
ss -tp Socket-Inode zu Prozess und Verbindung Nur Netzwerk-Sockets Ja
strace -e trace=open,close Zeigt exakten Open/Close-Codepfad Merklicher Overhead Meist vorinstalliert

In der Praxis beginnt die Untersuchung mit lsof oder /proc/[pid]/fd für die schnelle Kategorisierung nach Typ und Pfad, gefolgt von ss -tp für die genaue Zuordnung von Netzwerk-Sockets. Erst wenn diese beiden Schritte den verdächtigen Ressourcentyp eingegrenzt haben, lohnt sich der gezielte Einsatz von strace, um den exakten Codepfad zu bestätigen. Diese Reihenfolge verhindert, dass man mit dem aufwendigsten Werkzeug beginnt, obwohl die einfacheren Mittel bereits ausreichend Hinweise liefern.

Mironsoft

Linux-Troubleshooting und Server-Diagnose für Magento- und PHP-Infrastruktur

Wiederkehrende "Too many open files"-Fehler?

Wir analysieren eure PHP-FPM- und Nginx-Prozesse, finden das eigentliche File-Descriptor-Leck mit lsof und strace und richten Limits sowie Monitoring so ein, dass der nächste Vorfall früh sichtbar wird statt den Server unerwartet lahmzulegen.

FD-Audit

Systematische Zählung und Kategorisierung offener Deskriptoren pro Dienst

Ursachenanalyse

strace-gestützte Identifikation des genauen Codepfads hinter dem Leck

Limits & Monitoring

Passende ulimit- und systemd-Konfiguration mit frühzeitigem Alarmwert

10. Zusammenfassung

Der Fehler "Too many open files" ist fast immer ein Symptom für ein zugrunde liegendes File-Descriptor-Leck und selten ein reines Kapazitätsproblem. Die systematische Untersuchung beginnt mit dem Verständnis der beteiligten Limits, ulimit, systemd LimitNOFILE und der systemweiten Obergrenze aus /proc/sys/fs/file-max, gefolgt von einer Zählung und Kategorisierung offener Deskriptoren mit lsof oder /proc/[pid]/fd. Eine Zeitreihe über Stunden bestätigt, ob tatsächlich ein Leck vorliegt, während strace den genauen Codepfad hinter fehlenden close()-Aufrufen identifiziert.

In PHP-basierten Setups sind nicht geschlossene Datenbankverbindungen und cURL-Handles die häufigsten Ursachen, während bei Nginx eine Diskrepanz zwischen worker_connections und worker_rlimit_nofile oft mit einem echten Leck verwechselt wird. Wer Limits als Absicherung mit Monitoring-Alarmen kombiniert und die eigentliche Ursache im Code dokumentiert nachverfolgt, verhindert, dass "Too many open files" zu einem wiederkehrenden, nie wirklich gelösten Vorfall wird.

Too many open files debuggen: Das Wichtigste auf einen Blick

Fehlerbedeutung

EMFILE: Pro-Prozess-Limit erreicht. ENFILE: systemweites Limit erreicht. Fast immer ein Leck, kein reines Kapazitätsproblem.

Erste Werkzeuge

lsof -p PID für Typ und Pfad, /proc/[pid]/fd als Bordmittel-Alternative ohne Zusatzsoftware.

Häufigste Ursache

Nicht geschlossene Datenbankverbindungen und cURL-Handles in PHP-Anwendungen bei Fehlerpfaden.

Absicherung

LimitNOFILE großzügig setzen, aber mit Monitoring-Alarm bei 80 Prozent Auslastung kombinieren.

11. FAQ: "Too many open files" systematisch debuggen

1Unterschied EMFILE vs. ENFILE?
EMFILE: Pro-Prozess-Limit erreicht. ENFILE: systemweite Obergrenze über alle Prozesse erschöpft.
2Reicht es, das Limit zu erhöhen?
Nur kurzfristig. Meist liegt ein echtes Leck vor, ein höheres Limit verzögert nur den nächsten Absturz.
3Warum wirkt limits.conf nicht bei systemd?
PAM-Limits und Systemd-Limits sind getrennte Mechanismen. LimitNOFILE in der Unit-Datei zählt, nicht das Shell-Limit.
4Zählen ohne lsof?
ls -la /proc/[pid]/fd liefert dieselbe Liste, jeder Eintrag ein Symlink, auslesbar mit readlink.
5Was bedeutet (deleted) im Fd-Ziel?
Datei bereits gelöscht, belegt aber weiter Platz, weil ein Prozess sie offen hält. Häufig bei Log-Rotation ohne Reopen.
6Prozess hinter Socket ohne Pfad finden?
Inode-Nummer mit ss -tp abgleichen, das für TCP-Verbindungen Inode und Prozess anzeigt.
7Typische Ursachen in PHP?
Nicht geschlossene persistente PDO-Verbindungen und cURL-Handles bei Exceptions vor curl_close().
8Nginx worker_rlimit_nofile Problem erkennen?
Ist worker_connections höher als worker_rlimit_nofile, meldet Nginx die Diskrepanz direkt im Fehler-Log.
9Wie beweise ich ein Leck?
Mit einer Zeitreihe über Stunden, die monotones, zur Requestzahl proportionales Wachstum zeigt.
10Wann Monitoring-Alarm einrichten?
Bei rund 80 Prozent des konfigurierten Limits, um genug Zeit für die Ursachenanalyse zu gewinnen.