Von Soft-Limits über limits.conf bis zum systemd-Override
Wenn PHP-FPM oder Nginx unter Last mit Too many open files abbricht, liegt die Ursache fast immer an einem zu niedrigen ulimit-Wert statt an echtem Ressourcenmangel. Dieser Artikel erklärt den Unterschied zwischen Soft- und Hard-Limits, zeigt, wie man File-Descriptor- und Prozess-Grenzen über limits.conf und systemd-Overrides dauerhaft und korrekt konfiguriert, und wie man den Fehler zuverlässig diagnostiziert.
Inhaltsverzeichnis
- 1. Was ulimit wirklich begrenzt: Soft- und Hard-Limits
- 2. Der ulimit-Befehl: Syntax, Shell-Scope und wichtige Flags
- 3. Warum PHP- und Webserver-Workloads besonders betroffen sind
- 4. Persistente Limits über /etc/security/limits.conf setzen
- 5. systemd-Unit-Overrides: LimitNOFILE und LimitNPROC
- 6. Too many open files diagnostizieren und beheben
- 7. Nginx, PHP-FPM und Container-Workloads richtig dimensionieren
- 8. Kernel-weite Grenzen: fs.file-max und PAM-Limits
- 9. ulimit-Konfiguration im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was ulimit wirklich begrenzt: Soft- und Hard-Limits
ulimit ist die Schnittstelle der Shell zu den Ressourcengrenzen, die der Linux-Kernel jedem Prozess auferlegt: maximale Anzahl offener Dateien, maximale Anzahl gleichzeitiger Prozesse eines Benutzers, maximale Stack-Größe und mehrere weitere Kontingente. Jede dieser Grenzen existiert in zwei Ausprägungen. Das Soft-Limit ist der aktuell wirksame Wert, den ein Prozess ohne besondere Rechte selbst erhöhen kann, solange er das Hard-Limit nicht überschreitet. Das Hard-Limit ist die absolute Obergrenze, die nur root senken oder anheben darf und die als Deckel für das Soft-Limit fungiert.
Diese Zweiteilung existiert bewusst: Anwendungen sollen sich innerhalb eines sicheren Rahmens selbst an ihren tatsächlichen Bedarf anpassen können, ohne dass jeder Prozess root-Rechte braucht, um mehr Ressourcen anzufordern. In der Praxis bedeutet das, dass ein PHP-FPM-Worker sein Soft-Limit für offene Dateien zur Laufzeit auf bis zu 65536 hochsetzen könnte, sofern das Hard-Limit dies erlaubt, es aber niemals von selbst tut, wenn niemand die Limits explizit konfiguriert hat. Genau dort entsteht die häufigste Fehlerquelle: Der Standardwert vieler Distributionen liegt bei 1024 offenen Dateien pro Prozess, ein Wert, der für einen Webserver mit hoher gleichzeitiger Verbindungsanzahl viel zu niedrig ist.
2. Der ulimit-Befehl: Syntax, Shell-Scope und wichtige Flags
Der Befehl ulimit -a zeigt alle aktuell gesetzten Limits der laufenden Shell an, jeweils mit Soft-Wert, sofern nicht anders angegeben. Mit -S und -H lässt sich gezielt zwischen Soft- und Hard-Limit unterscheiden, etwa ulimit -Sn für das aktuelle Soft-Limit offener Dateien und ulimit -Hn für das entsprechende Hard-Limit. Wichtig ist dabei der Geltungsbereich: ulimit, in einer interaktiven Shell gesetzt, gilt ausschließlich für diese Shell und alle aus ihr gestarteten Kindprozesse, nicht für bereits laufende Prozesse und nicht für Prozesse, die von systemd oder einem Display-Manager unabhängig von der Shell gestartet werden.
Das -n-Flag steuert RLIMIT_NOFILE, also die maximale Anzahl offener File-Descriptors, das -u-Flag steuert RLIMIT_NPROC, die maximale Anzahl Prozesse und Threads eines Benutzers. Beide Werte sind für Webserver-Workloads am relevantesten, weil jede TCP-Verbindung, jede geöffnete Datei und jeder Unix-Socket einen File-Descriptor belegt, und jeder PHP-FPM-Worker sowie jeder Thread eines Prozesses gegen das Prozess-Limit zählt. Ein manuell in der Shell gesetztes Limit überlebt weder einen Neustart des Dienstes noch einen Server-Reboot, weshalb es für produktive Systeme nur als temporärer Test taugt.
# Show all current limits of the running shell
$ ulimit -a
core file size (blocks, -c) 0
open files (-n) 1024
max user processes (-u) 15938
stack size (kbytes, -s) 8192
# Inspect soft vs. hard limit for open file descriptors
$ ulimit -Sn
1024
$ ulimit -Hn
1048576
# Raise the soft limit within the hard limit ceiling (session-scoped only)
$ ulimit -n 65536
$ ulimit -Sn
65536
3. Warum PHP- und Webserver-Workloads besonders betroffen sind
PHP-FPM startet pro konfiguriertem Worker einen eigenen Prozess, und jeder dieser Worker hält Datenbankverbindungen, Cache-Verbindungen zu Redis oder Memcached, geöffnete Log-Dateien und die eigentliche Client-Verbindung gleichzeitig offen. Bei pm.max_children = 50 und mehreren offenen Verbindungen pro Worker summiert sich der Bedarf an File-Descriptors schnell auf mehrere Tausend, weit über dem Standardlimit von 1024. Sobald ein Worker sein Limit erreicht, schlägt jede weitere fopen()-, fsockopen()- oder Datenbankverbindung mit einem EMFILE-Fehler fehl, den PHP als Warnung oder Exception meldet.
Aehnlich kritisch ist das Prozess-Limit bei Anwendungen, die intern Threads oder Subprozesse erzeugen, etwa Queue-Worker, die parallele Jobs verarbeiten, oder Build-Pipelines mit vielen gleichzeitigen Kindprozessen. Nginx als vorgeschalteter Reverse Proxy hält zusätzlich eigene File-Descriptors für jede Client-Verbindung, jeden Upstream-Socket zu PHP-FPM und jede geöffnete Log-Datei. Bei hoher gleichzeitiger Nutzerzahl treffen deshalb Nginx und PHP-FPM oft zeitgleich auf ihre jeweiligen Limits, was die Fehlersuche erschwert, wenn nicht beide Dienste unabhängig voneinander geprüft werden.
4. Persistente Limits über /etc/security/limits.conf setzen
Damit ein Limit einen Neustart übersteht, muss es über das PAM-Modul pam_limits konfiguriert werden, dessen zentrale Konfigurationsdatei /etc/security/limits.conf ist. Jede Zeile besteht aus vier Feldern: Domain (Benutzer, Gruppe mit @-Präfix oder * für alle), Typ (soft, hard oder - für beide gleichzeitig), Ressource (nofile, nproc und weitere) und der eigentliche Wert. Diese Datei wirkt jedoch ausschließlich auf Prozesse, die über einen PAM-gesteuerten Login-Pfad gestartet werden, also interaktive SSH-Sessions oder su/login-basierte Prozesse.
Genau diese Einschränkung ist eine häufige Fehlerquelle: Ein Administrator setzt in limits.conf großzügige Werte für den Benutzer www-data, meldet sich zum Testen per SSH als www-data an und sieht dort korrekt das neue Limit. Der eigentliche PHP-FPM-Prozess, von systemd direkt beim Booten gestartet, durchläuft aber niemals den PAM-Login-Pfad und ignoriert limits.conf deshalb vollständig. Zusätzlich muss in /etc/pam.d/common-session die Zeile session required pam_limits.so vorhanden sein, sonst greift die Konfiguration selbst bei echten Logins nicht.
# /etc/security/limits.conf: persistent limits for the www-data user
# domain type item value
www-data soft nofile 65536
www-data hard nofile 131072
www-data soft nproc 4096
www-data hard nproc 8192
# Group-wide limit for all members of the docker group
@docker soft nofile 65536
@docker hard nofile 131072
# Applies only to PAM-driven login paths (ssh, su, login)
# NOT applied to services started directly by systemd
5. systemd-Unit-Overrides: LimitNOFILE und LimitNPROC
Weil die meisten produktiven Dienste heute direkt über systemd gestartet werden und nicht über einen PAM-Login-Pfad, ist limits.conf für sie faktisch wirkungslos. Der korrekte Ort für Ressourcengrenzen von Diensten wie PHP-FPM oder Nginx ist die Unit-Datei selbst, konkret die Direktiven LimitNOFILE und LimitNPROC im [Service]-Abschnitt. Statt die vom Paketmanager verwaltete Unit-Datei direkt zu editieren, was bei einem Paket-Update überschrieben würde, erstellt man mit systemctl edit php8.4-fpm.service ein Override-Fragment, das systemd automatisch zusammen mit der Original-Unit lädt.
Nach jeder Aenderung an einer Unit-Datei oder einem Override muss systemctl daemon-reload ausgeführt werden, damit systemd die neue Konfiguration einliest, gefolgt von einem Neustart des betroffenen Dienstes. Mit systemctl show php8.4-fpm.service -p LimitNOFILE lässt sich prüfen, welcher Wert tatsächlich aktiv ist, und mit cat /proc/$(pgrep -f 'php-fpm: master')/limits sieht man das Limit direkt aus Sicht des laufenden Prozesses, was der zuverlässigste Nachweis ist, dass die Konfiguration tatsächlich greift.
# /etc/systemd/system/php8.4-fpm.service.d/override.conf
# Created via: systemctl edit php8.4-fpm.service
[Service]
LimitNOFILE=65536
LimitNPROC=4096
# Same pattern for Nginx
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65536
# Apply the change:
# systemctl daemon-reload
# systemctl restart php8.4-fpm nginx
# systemctl show php8.4-fpm.service -p LimitNOFILE
6. Too many open files diagnostizieren und beheben
Die Fehlermeldung Too many open files beziehungsweise der Kernel-seitige Errno-Code EMFILE bedeutet, dass ein Prozess sein individuelles File-Descriptor-Limit erreicht hat, nicht dass dem System selbst die Ressourcen ausgegangen sind. Der erste Diagnoseschritt ist, die betroffene Prozess-ID zu ermitteln, etwa aus dem PHP-FPM- oder Nginx-Error-Log, und anschließend mit cat /proc/PID/limits das tatsächlich aktive Soft- und Hard-Limit für Max open files auszulesen. Mit ls /proc/PID/fd | wc -l zählt man die aktuell geöffneten File-Descriptors dieses Prozesses und vergleicht den Wert direkt mit dem Limit.
Ergänzend zeigt lsof -p PID im Detail, welche Dateien, Sockets und Pipes ein Prozess offen hält, was bei einem File-Descriptor-Leck häufig verrät, dass eine Datenbankverbindung oder ein Socket nie geschlossen wird. Systemweit zeigt lsof | wc -l die Gesamtzahl offener Deskriptoren aller Prozesse und lässt sich mit dem Kernel-weiten Limit aus cat /proc/sys/fs/file-nr vergleichen. Steigt die Anzahl offener Deskriptoren eines einzelnen Prozesses über die Zeit kontinuierlich an, ohne je zu sinken, liegt fast immer ein Leck im Anwendungscode vor, das durch eine Limit-Erhöhung nur hinausgezögert, nicht behoben wird.
#!/usr/bin/env bash
# Diagnose an EMFILE / Too many open files incident
set -euo pipefail
PID=$(pgrep -f 'php-fpm: pool www' | head -n1)
echo "--- Configured limits for PID $PID ---"
grep -i "open files" "/proc/${PID}/limits"
echo "--- Currently open file descriptors ---"
ls "/proc/${PID}/fd" | wc -l
echo "--- Top 10 open file/socket types ---"
lsof -p "$PID" | awk '{print $5}' | sort | uniq -c | sort -rn | head -10
echo "--- System-wide open file handles vs. kernel limit ---"
cat /proc/sys/fs/file-nr
7. Nginx, PHP-FPM und Container-Workloads richtig dimensionieren
Nginx bringt mit worker_rlimit_nofile eine eigene Konfigurationsdirektive mit, die das Prozesslimit überschreibt, sofern der Wert das systemd-Hard-Limit nicht übersteigt. Diese Direktive ist sinnvoll, weil sie die Grenze direkt im Anwendungskontext dokumentiert, ersetzt aber nicht die systemd-Konfiguration, sondern ergänzt sie: Nginx kann sein Soft-Limit nur bis zum vom Betriebssystem vorgegebenen Hard-Limit anheben. Bei PHP-FPM gilt eine zusätzliche Faustregel: Die Anzahl der Worker aus pm.max_children multipliziert mit den durchschnittlich pro Worker offenen Verbindungen sollte das konfigurierte LimitNOFILE nie ausreizen, sondern mit deutlichem Puffer darunter bleiben.
In containerisierten Umgebungen gelten dieselben Kernel-Mechanismen, aber die Konfiguration erfolgt über die Container-Runtime statt über systemd auf dem Host. Docker setzt ohne explizite Angabe oft ein zu niedriges Default-Limit, weshalb Limits entweder im docker-compose.yml über den ulimits-Schlüssel oder direkt beim docker run-Aufruf mit --ulimit nofile=65536:65536 gesetzt werden sollten. Wichtig ist, dass ein im Container gesetztes Limit niemals das Host-Hard-Limit übersteigen kann, weil Container-Prozesse letztlich Prozesse des Host-Kernels bleiben.
# docker-compose.yml: raise file descriptor and process limits per service
services:
php-fpm:
image: mironsoft/php:8.4-fpm
ulimits:
nofile:
soft: 65536
hard: 131072
nproc: 4096
nginx:
condition: service_healthy
nginx:
image: nginx:1.27
ulimits:
nofile:
soft: 65536
hard: 65536
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
8. Kernel-weite Grenzen: fs.file-max und PAM-Limits
Ueber allen prozessspezifischen ulimit-Werten liegt eine systemweite Obergrenze, die der Kernel-Parameter fs.file-max festlegt: die maximale Anzahl gleichzeitig geöffneter Dateien über alle Prozesse des gesamten Systems hinweg. Dieser Wert ist auf modernen Systemen mit ausreichend RAM meist großzügig voreingestellt, kann aber auf kleineren Cloud-Instanzen oder Containern mit begrenztem Speicher zu niedrig ausfallen. Mit cat /proc/sys/fs/file-max lässt sich der aktuelle Wert prüfen, und mit cat /proc/sys/fs/file-nr sieht man die aktuell genutzte, die zugewiesene und die maximale Anzahl im Vergleich.
Eine dauerhafte Anpassung erfolgt über /etc/sysctl.conf oder eine eigene Datei unter /etc/sysctl.d/, gefolgt von sysctl -p, um die Aenderung ohne Neustart zu aktivieren. Wichtig ist die Rangfolge der Limits: Ein einzelner Prozess kann niemals mehr Dateien öffnen, als sein eigenes RLIMIT_NOFILE erlaubt, selbst wenn fs.file-max theoretisch noch Kapazität hätte, und umgekehrt bremst ein zu niedriges fs.file-max alle Prozesse gemeinsam, unabhängig von deren individuell gesetzten, höheren ulimit-Werten. Beide Ebenen müssen deshalb konsistent aufeinander abgestimmt sein, sonst bleibt eine großzügig gesetzte Prozessgrenze wirkungslos.
9. ulimit-Konfiguration im Vergleich
Je nachdem, wie ein Dienst gestartet wird und welches Werkzeug zur Limit-Konfiguration genutzt wird, entscheidet sich, ob eine Grenze tatsächlich greift oder stillschweigend ignoriert wird. Die folgende Uebersicht fasst die häufigsten Fehlkonfigurationen und die jeweils korrekte Vorgehensweise zusammen.
| Szenario | Problem | Empfohlene Massnahme | Effekt |
|---|---|---|---|
| Manuell per ulimit in der Shell | Gilt nur für die aktuelle Session | limits.conf oder systemd-Override | Uebersteht Neustart und Reboot |
| Dienst wird von systemd gestartet | limits.conf wird komplett ignoriert | LimitNOFILE in Unit-Override |
Limit greift direkt beim Prozessstart |
| pm.max_children überschreitet ulimit -u | Worker-Spawn schlägt sporadisch fehl | nproc-Limit an Worker-Anzahl anpassen | Keine fehlenden Prozesse unter Last |
| Docker-Container ohne ulimits-Angabe | Zu niedriges Default-FD-Limit | --ulimit nofile=65536:65536 |
Konsistentes Limit über alle Container |
| Monitoring nur CPU und RAM | FD-Erschöpfung bleibt unbemerkt | /proc/PID/limits als Alert-Metrik | EMFILE-Fehler vor Ausfall erkannt |
Auffällig ist, dass fast jede Zeile der Tabelle auf denselben Grundfehler zurückführt: ein Limit wird an der falschen Stelle gesetzt und deshalb vom tatsächlich laufenden Prozess niemals gelesen. Wer Limits konsequent über systemd-Overrides für Dienste und limits.conf für interaktive Logins trennt und beide Ebenen regelmäßig mit /proc/PID/limits verifiziert, vermeidet den kompletten Fehlerkatalog aus dieser Uebersicht.
Mironsoft
Server-Administration, Kapazitätsplanung und Magento-Infrastruktur
Ressourcen-Limits einmal richtig konfiguriert?
Wir analysieren eure PHP-FPM- und Nginx-Konfiguration, setzen File-Descriptor- und Prozess-Limits über systemd-Overrides sauber und persistent auf und richten Monitoring ein, damit Too many open files nie wieder unerwartet die Produktion trifft.
Limit-Audit
Soft- und Hard-Limits aller produktiven Dienste prüfen und dokumentieren
systemd-Konfiguration
LimitNOFILE und LimitNPROC als saubere Unit-Overrides einrichten
FD-Monitoring
Frühwarnung vor EMFILE-Fehlern in Grafana und Alertmanager
10. Zusammenfassung
Die wichtigste Erkenntnis rund um System-Ressourcen-Limits mit ulimit: Soft- und Hard-Limits sind zwei getrennte Werte, und wo ein Limit gesetzt wird, entscheidet, ob es beim tatsächlichen Prozessstart überhaupt wirkt. ulimit in der Shell gesetzt gilt nur für die aktuelle Session, /etc/security/limits.conf nur für PAM-gesteuerte Logins, und Dienste, die direkt von systemd gestartet werden, ignorieren beide vollständig, wenn kein LimitNOFILE oder LimitNPROC in der Unit-Datei gesetzt ist. Genau diese Verwechslung ist die häufigste Ursache dafür, dass ein scheinbar korrekt gesetztes Limit in der Produktion trotzdem nicht ankommt.
Die Fehlermeldung Too many open files lässt sich mit /proc/PID/limits, lsof -p PID und einem Vergleich gegen fs.file-max in wenigen Minuten sicher diagnostizieren, statt raten zu müssen, ob es sich um einen Kapazitäts- oder einen Konfigurationsfehler handelt. Wer systemd-Overrides für alle produktiven Dienste konsequent nutzt, PHP-FPM- und Nginx-Limits an die tatsächliche Worker-Anzahl anpasst und File-Descriptor-Nutzung als eigene Monitoring-Metrik neben CPU und RAM etabliert, verhindert, dass diese Fehlermeldung unter Last überhaupt erst auftritt.
System-Ressourcen-Limits mit ulimit, das Wichtigste auf einen Blick
Soft vs. Hard
Das Soft-Limit ist der aktive Wert, das Hard-Limit die Obergrenze. Prozesse dürfen das Soft-Limit selbst bis zum Hard-Limit erhöhen.
limits.conf vs. systemd
limits.conf wirkt nur bei PAM-Logins. Von systemd gestartete Dienste brauchen zwingend LimitNOFILE und LimitNPROC im Unit-Override.
Too many open files
Mit /proc/PID/limits und lsof -p PID in Minuten diagnostizierbar. Ein ständig steigender FD-Zähler deutet auf ein Leck im Code hin.
Kernel-weites Limit
fs.file-max begrenzt alle Prozesse gemeinsam und muss zum Summenbedarf aller Dienste konsistent passen.