Skip to content

Split View: Too many open files 완전 해결 — ulimit을 올려도 안 되는 이유

✨ Learn with Quiz
|

Too many open files 완전 해결 — ulimit을 올려도 안 되는 이유

들어가며 — accept4() failed (24: Too many open files)

증상은 이렇게 나타납니다.

2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)
2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)

또는 자바라면 이렇습니다.

java.net.SocketException: Too many open files
	at java.base/sun.nio.ch.Net.accept(Native Method)
Caused by: java.io.IOException: Too many open files

여기서 거의 모든 사람이 같은 일을 합니다. ulimit -n 65536을 실행하고, 서비스를 재시작하고, 여전히 같은 에러를 봅니다. 그 이유는 한계가 하나가 아니라 셋이고, 셸에서 바꾼 값이 서비스에는 적용되지 않기 때문입니다.

errno 24는 EMFILE입니다. 문구를 정확히 읽어야 합니다.

errno 24 23
# EMFILE 24 Too many open files
# ENFILE 23 Too many open files in system

in system이 붙으면 다른 문제입니다. 이 차이 하나가 진단의 절반을 갈라 줍니다.

한계는 하나가 아니라 셋입니다

세 가지가 각각 독립적으로 존재하고, 어느 하나라도 걸리면 파일을 열 수 없습니다.

한계범위초과 시 errno확인설정 위치
RLIMIT_NOFILE soft프로세스 하나EMFILE (24)/proc/PID/limitsulimit -n, limits.conf, systemd LimitNOFILE
RLIMIT_NOFILE hard프로세스 하나설정 거부/proc/PID/limits위와 같음, 낮추기만 비특권 가능
fs.nr_open커널 전역EPERM/EINVALsysctl fs.nr_open/etc/sysctl.d/
fs.file-max시스템 전체ENFILE (23)cat /proc/sys/fs/file-nr/etc/sysctl.d/

관계를 정리하면 이렇습니다.

fs.file-max   : 시스템 전체가 동시에 열 수 있는 파일 개수 (모든 프로세스의 합)
   └─ fs.nr_open : 한 프로세스의 RLIMIT_NOFILE이 넘을 수 없는 천장
        └─ RLIMIT_NOFILE hard : 관리자가 정한 그 프로세스의 상한
             └─ RLIMIT_NOFILE soft : 실제로 적용되는 값. 프로세스가 hard까지 스스로 올릴 수 있음

현재 상태를 한 번에 봅니다.

sysctl fs.file-max fs.nr_open
# fs.file-max = 6553600
# fs.nr_open = 1048576

cat /proc/sys/fs/file-nr
# 13984	0	6553600

file-nr의 세 숫자는 각각 할당된 파일 핸들 수, 할당됐지만 미사용인 수(최근 커널에서는 항상 0), 최대값입니다. 첫 번째가 세 번째에 근접하면 시스템 전역 문제이고 이때 나오는 것이 ENFILE입니다. 위 예에서는 13984 대 655만이므로 여유가 압도적입니다. 대부분의 현대 리눅스에서 fs.file-max는 병목이 아닙니다. 커널이 메모리 크기에 비례해 자동 계산하기 때문입니다. 그런데도 많은 튜닝 문서가 이 값부터 올리라고 합니다. 올려도 무해하지만 증상은 그대로입니다.

fs.nr_open은 다릅니다. 기본값 1048576을 넘는 ulimit -n은 설정 자체가 실패합니다.

ulimit -n 2000000
# bash: ulimit: open files: cannot modify limit: Operation not permitted

sudo sysctl -w fs.nr_open=2097152
# fs.nr_open = 2097152
ulimit -n 2000000   # 이제 성공합니다

실제로 적용된 값 확인 — ulimit이 아니라 /proc/PID/limits

여기가 핵심입니다. ulimit -n지금 이 셸의 값을 보여 줍니다. 문제가 나는 프로세스의 값이 아닙니다. 둘은 자주 다릅니다.

ulimit -n
# 1048576

pgrep -x nginx
# 14022
# 14025

sudo grep 'Max open files' /proc/14025/limits
# Max open files            1024                 524288               files

셸은 100만인데 nginx 워커의 soft 한계는 1024입니다. 이 차이가 문제의 정체입니다. 앞으로 이 증상을 만나면 ulimit -n은 아예 보지 말고 이 명령부터 실행하십시오.

# 프로세스 이름으로 한 번에
for pid in $(pgrep -x nginx); do
  printf '%6s  %s\n' "$pid" "$(awk '/Max open files/ {print $4, $5}' /proc/"$pid"/limits)"
done
#  14022  1024 524288
#  14025  1024 524288

지금 몇 개를 쓰고 있는지도 같이 봅니다.

# 실제 열린 fd 개수 (가장 정확)
sudo ls /proc/14025/fd | wc -l
# 1021

# 사용률을 한 줄로
for pid in $(pgrep -x nginx); do
  used=$(sudo ls /proc/"$pid"/fd 2>/dev/null | wc -l)
  soft=$(awk '/Max open files/ {print $4}' /proc/"$pid"/limits)
  printf '%6s  %6s / %-8s (%d%%)\n' "$pid" "$used" "$soft" $((used * 100 / soft))
done
#  14022    1021 / 1024     (99%)
#  14025    1019 / 1024     (99%)

lsof -p PID | wc -l을 쓰면 숫자가 더 크게 나옵니다. lsof는 현재 작업 디렉터리, 루트 디렉터리, 실행 파일, 메모리 매핑된 라이브러리까지 함께 세는데 이것들은 RLIMIT_NOFILE에 포함되지 않습니다. 한계와 비교할 때는 /proc/PID/fd의 개수를 쓰십시오.

systemd가 ulimit 설정을 무시하는 이유

가장 흔한 함정입니다. 다음 설정을 넣고 재시작했는데 아무것도 바뀌지 않은 경험이 있다면 이 절이 답입니다.

cat /etc/security/limits.d/99-nofile.conf
# *    soft    nofile    1048576
# *    hard    nofile    1048576

이 파일은 PAM 모듈 pam_limits가 읽습니다. 그리고 pam_limits로그인 세션에서만 동작합니다. SSH 접속, su, login이 여기 해당합니다. systemd가 부팅 시 띄우는 시스템 서비스는 로그인 세션이 아니므로 이 파일을 아예 읽지 않습니다.

서비스의 한계를 정하는 것은 systemd입니다. 우선순위는 유닛의 LimitNOFILE, 없으면 /etc/systemd/system.confDefaultLimitNOFILE, 그것도 없으면 systemd 내장 기본값입니다.

# 유닛에 적용된 값 (하드와 소프트가 따로 노출됩니다)
systemctl show -p LimitNOFILE -p LimitNOFILESoft nginx.service
# LimitNOFILE=524288
# LimitNOFILESoft=1024

# 전역 기본값
systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft
# DefaultLimitNOFILE=524288
# DefaultLimitNOFILESoft=1024

여기서 systemd 240 이후의 기본 동작을 알아 둘 필요가 있습니다. systemd는 하드 한계를 524288로 크게 잡아 두고 소프트 한계는 1024로 낮게 유지합니다. 이유는 호환성입니다. select()는 fd 번호 1024 이상을 다루지 못하고, 일부 오래된 프로그램은 시작할 때 0부터 소프트 한계까지 모든 fd를 닫는 루프를 도는데 그 한계가 100만이면 기동이 몇 초씩 걸립니다. 그래서 "필요한 프로그램이 스스로 올려 쓰라"는 것이 systemd의 설계 의도입니다.

실제로 현대적인 런타임 상당수가 그렇게 합니다. Go는 런타임이 시작 시 소프트를 하드까지 올리고, 그래서 같은 머신에서 Go 서비스는 멀쩡한데 다른 서비스만 EMFILE이 나는 상황이 생깁니다.

올바른 설정 방법입니다.

sudo systemctl edit nginx.service
# 열린 편집기에 아래를 씁니다
# [Service]
# LimitNOFILE=65536

sudo systemctl daemon-reload
sudo systemctl restart nginx

두 가지를 강조합니다. systemctl reload로는 rlimit이 바뀌지 않습니다. 프로세스 생성 시점에 적용되는 값이므로 반드시 restart여야 합니다. 그리고 LimitNOFILE=65536처럼 값을 하나만 쓰면 소프트와 하드가 모두 그 값이 됩니다. 따로 주려면 LimitNOFILE=65536:524288 형식을 씁니다.

적용을 확인합니다.

sudo grep 'Max open files' /proc/$(pgrep -x nginx | head -1)/limits
# Max open files            65536                65536                files

애플리케이션 자체 설정이 또 있는 경우도 있습니다. nginx는 worker_rlimit_nofile이라는 지시어를 따로 가지고 있고, 이 값이 systemd가 준 하드 한계를 넘지 못합니다.

# /etc/nginx/nginx.conf
worker_rlimit_nofile 65536;
events {
    worker_connections 32768;
}

worker_connectionsworker_rlimit_nofile보다 크면 의미가 없습니다. 연결 하나가 클라이언트 소켓 하나와 업스트림 소켓 하나를 쓸 수 있으므로, 리버스 프록시라면 worker_rlimit_nofileworker_connections의 두 배 이상으로 잡는 것이 안전합니다.

재시작 없이 급한 불을 끄는 방법도 있습니다.

sudo prlimit --pid 14025 --nofile=65536:65536
sudo prlimit --pid 14025 --nofile
# RESOURCE DESCRIPTION                             SOFT  HARD UNITS
# NOFILE   max number of open files               65536 65536 files

prlimit은 실행 중인 프로세스의 한계를 바꿉니다. 다만 애플리케이션이 기동 시에 한계를 읽어 내부 자료구조 크기를 정했다면(nginx의 커넥션 풀이 그렇습니다) 효과가 없을 수 있습니다. 진짜 해결은 유닛 파일 수정과 재시작입니다.

컨테이너의 규칙은 또 다릅니다. 컨테이너 안 프로세스의 RLIMIT_NOFILE은 OCI 런타임 스펙의 process.rlimits에서 오고, 그 값은 컨테이너 런타임 데몬이 정합니다. 셸의 ulimit도, 호스트의 limits.conf도 관여하지 않습니다.

# 컨테이너 안에서 확인하는 것이 유일하게 확실한 방법입니다
docker exec api-server cat /proc/1/limits | grep 'Max open files'
# Max open files            1048576              1048576              files

# 컨테이너별로 지정
docker run --ulimit nofile=65536:65536 myimage

# 데몬 전역 기본값
cat /etc/docker/daemon.json
# {
#   "default-ulimits": { "nofile": { "Name": "nofile", "Soft": 65536, "Hard": 65536 } }
# }

쿠버네티스 파드 스펙에는 ulimit 필드가 없습니다. 파드 단위로 조정하려면 컨테이너 진입점에서 ulimit -n으로 낮추거나(하드 이하로만 가능), 노드의 containerd 설정을 바꾸거나, 컨테이너 런타임 유닛의 LimitNOFILE을 조정해야 합니다. 런타임 유닛이 LimitNOFILE=infinity인 배포판이 많고, 그 경우 컨테이너가 매우 큰 값을 물려받습니다. 넉넉해서 좋아 보이지만 앞서 말한 "fd 전부 닫기" 루프를 도는 프로그램에서는 기동이 수십 초 걸리는 원인이 됩니다.

무엇이 fd를 먹고 있는가 — 소켓, 파일, 파이프 세분

한계를 올리기 전에 무엇이 차지하는지 봐야 합니다. /proc/PID/fd의 심볼릭 링크 대상이 종류를 알려 줍니다.

sudo ls -l /proc/14025/fd | awk '{print $NF}' \
  | sed -E 's#^(socket|pipe|anon_inode):.*#\1#; s#^/.*#regular-file#' \
  | sort | uniq -c | sort -rn
#   61204 socket
#    3891 regular-file
#     412 anon_inode
#       8 pipe
#       3 /dev/null

소켓이 6만 개면 네트워크 쪽입니다. 어떤 소켓인지 더 파고듭니다.

# 상태별 소켓 개수
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
#   38210 ESTAB
#   21044 CLOSE-WAIT
#    2118 TIME-WAIT
#     412 LISTEN

CLOSE-WAIT이 2만 개입니다. 이것이 fd 누수의 결정적 증거입니다. CLOSE-WAIT은 상대가 FIN을 보내 연결을 닫았는데 우리 쪽 애플리케이션이 close()를 호출하지 않은 상태입니다. 커널은 애플리케이션이 닫아 주기를 기다리며 무한정 이 상태로 남습니다. 타임아웃이 없습니다. TIME-WAIT과 혼동하지 마십시오. TIME-WAIT은 커널이 알아서 정리하고 fd도 소비하지 않습니다.

어느 상대와의 연결인지 보면 원인 코드가 좁혀집니다.

sudo ss -tanp state close-wait | head -5
# Recv-Q Send-Q   Local Address:Port      Peer Address:Port  Process
# 1      0        10.0.3.14:44120        10.0.9.31:6379     users:(("java",pid=21874,fd=1042))
# 1      0        10.0.3.14:44122        10.0.9.31:6379     users:(("java",pid=21874,fd=1043))

# 상대 주소별 집계
sudo ss -tan state close-wait | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
#   20981 10.0.9.31
#      63 10.0.9.42

전부 같은 상대(Redis)입니다. Redis 클라이언트 풀이 연결을 반환하지 않고 있다는 뜻이고, 여기서 코드 리뷰 대상이 결정됩니다.

파일 쪽이 범인이라면 어느 파일인지 셉니다.

sudo ls -l /proc/21874/fd | awk '$NF ~ /^\// {print $NF}' \
  | sed -E 's#/[^/]+$##' | sort | uniq -c | sort -rn | head -5
#    3204 /var/lib/app/uploads/tmp
#     512 /data/kafka/logs/orders-0
#      88 /usr/lib/x86_64-linux-gnu

/var/lib/app/uploads/tmp에 3204개가 열려 있습니다. 임시 파일을 만들고 닫지 않는 코드 경로가 있다는 뜻입니다.

inotify와 epoll은 별개의 한계입니다

anon_inode 계열은 RLIMIT_NOFILE도 소비하지만 자체 상한이 따로 있습니다. 그래서 fd에 여유가 충분한데도 실패하는 일이 생깁니다.

inotify의 상한은 세 개입니다.

sysctl fs.inotify
# fs.inotify.max_queued_events = 16384
# fs.inotify.max_user_instances = 128
# fs.inotify.max_user_watches = 65536

max_user_instances는 사용자당 만들 수 있는 inotify 인스턴스(inotify_init) 개수이고 초과하면 EMFILE입니다. ulimit -n과 문구가 완전히 같은 에러가 나지만 원인이 다릅니다. max_user_watches는 사용자당 감시 가능한 경로 개수이고 초과하면 ENOSPC, 즉 "No space left on device"가 나옵니다. 디스크와 무관한데 디스크 에러처럼 보입니다.

두 상한 모두 사용자 단위라는 점이 중요합니다. 같은 UID로 도는 컨테이너 수십 개가 하나의 예산을 나눠 씁니다. 노드에 파드를 늘렸더니 갑자기 감시 기반 도구가 죽는 전형적인 패턴이 여기서 나옵니다.

현재 사용량은 이렇게 셉니다.

# 인스턴스를 쥔 프로세스
sudo find /proc/[0-9]*/fd -lname 'anon_inode:*inotify*' 2>/dev/null \
  | cut -d/ -f3 | sort | uniq -c | sort -rn | head -5
#      42   9931
#       8   3311
#       2   1042

# 실제 watch 개수 (fdinfo의 inotify 줄 수가 곧 watch 수입니다)
sudo grep -c '^inotify' /proc/9931/fdinfo/* 2>/dev/null | grep -v ':0$' | head
# /proc/9931/fdinfo/18:31204
# /proc/9931/fdinfo/22:8102
cat /etc/sysctl.d/60-inotify.conf
# fs.inotify.max_user_watches = 524288
# fs.inotify.max_user_instances = 1024
sudo sysctl --system

epoll에도 별도 상한이 있습니다.

cat /proc/sys/fs/epoll/max_user_watches
# 3618421

이 값은 사용자당 등록 가능한 epoll 감시 항목 수이고, 커널이 저메모리(low memory)의 약 4%를 기준으로 자동 산정합니다. 항목 하나가 커널 메모리를 수십 바이트 쓰기 때문에 무제한이 아닌 것입니다. 실무에서 이 한계에 부딪히는 경우는 드물지만, 수백만 커넥션을 다루는 프록시라면 확인 대상입니다.

정리하면 이렇습니다.

자원상한단위초과 시 errno
일반 fdRLIMIT_NOFILE프로세스EMFILE (24)
시스템 전체 파일fs.file-max시스템ENFILE (23)
inotify 인스턴스fs.inotify.max_user_instances사용자EMFILE (24)
inotify watchfs.inotify.max_user_watches사용자ENOSPC (28)
epoll watchfs.epoll.max_user_watches사용자ENOSPC (28)

진짜 원인은 대개 누수 — 확인법과 응급 처치

한계를 올려서 해결되는 경우는 두 가지뿐입니다. 실제로 동시 연결이 그만큼 필요한 경우, 그리고 기본값이 비상식적으로 낮은 경우입니다. 나머지는 전부 누수이고, 누수에 한계를 올리는 것은 장애 시점을 몇 시간 뒤로 미루는 일입니다.

누수의 정의는 단순합니다. fd 사용량이 부하와 무관하게 단조 증가하면 누수입니다. 부하와 함께 오르내리면 정상적인 용량 문제입니다. 이것을 가르는 데는 몇 분간의 샘플링이면 충분합니다.

PID=21874
while sleep 60; do
  printf '%s  fd=%-7s estab=%-7s close_wait=%s\n' \
    "$(date +%H:%M:%S)" \
    "$(sudo ls /proc/$PID/fd | wc -l)" \
    "$(ss -tan state established | wc -l)" \
    "$(ss -tan state close-wait | wc -l)"
done
# 14:02:11  fd=18204   estab=8102    close_wait=9931
# 14:03:11  fd=19388   estab=8044    close_wait=11102
# 14:04:11  fd=20511   estab=7981    close_wait=12290
# 14:05:11  fd=21702   estab=8120    close_wait=13401

연결 수(estab)는 8000 근처에서 평평한데 fd는 분당 1200씩 오릅니다. 그리고 증가분이 close_wait의 증가분과 정확히 같습니다. 소켓 누수가 확정됐고, 앞 절의 ss -tanp로 상대까지 특정됐다면 조사할 코드 범위는 아주 좁습니다.

어느 코드 경로가 여는지까지 잡으려면 추적이 필요합니다.

# 열리는 파일과 호출 스택 (bcc 도구)
sudo opensnoop-bpfcc -p 21874

# fd를 여는 시스템 콜 빈도
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_openat /pid == 21874 && args->ret > 0/ { @[ustack] = count(); }'

# strace는 부하가 크므로 짧게만
sudo timeout 5 strace -f -e trace=openat,socket,close -p 21874 -c
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- ----------------
#  61.02    0.184203          18     10233           socket
#  38.44    0.116041          21      5488           openat
#   0.54    0.001632           4       402           close

socket이 10233번인데 close가 402번입니다. 비율만 봐도 답이 나옵니다.

응급 처치의 선택지는 세 가지이고 순서가 있습니다.

# 1) 프로세스 재시작 — 가장 확실하지만 서비스 중단
sudo systemctl restart myapp

# 2) prlimit으로 한계만 올려 시간을 벌기 (재시작 없음)
sudo prlimit --pid 21874 --nofile=200000:200000

# 3) 특정 fd를 강제로 닫기 — 최후의 수단, 애플리케이션이 깨질 수 있습니다
sudo gdb -p 21874 -batch -ex 'call (int)close(1042)'

3번은 애플리케이션이 그 fd를 여전히 유효하다고 믿고 있는 상태에서 닫는 것이므로 데이터 손상이나 크래시를 각오해야 합니다. 프로덕션에서 마지막으로 남은 선택지일 때만 쓰십시오.

마치며 — 세 개의 한계와 하나의 진짜 원인

하나만 기억한다면 이것입니다. ulimit -n은 지금 이 셸의 값이고 서비스와 아무 관계가 없습니다. 봐야 할 것은 언제나 /proc/PID/limits입니다.

진단 순서를 압축하면 이렇습니다.

  1. 에러 문구를 정확히 읽습니다. in system이 붙으면 fs.file-max, 아니면 프로세스 한계입니다.
  2. /proc/PID/limits로 실제 적용값을, ls /proc/PID/fd | wc -l로 실사용량을 봅니다. 이 둘만 있으면 한계 문제인지 아닌지 즉시 판정됩니다.
  3. systemd 서비스라면 limits.conf가 아니라 systemctl edit으로 LimitNOFILE을 설정하고 restart 합니다. reload로는 바뀌지 않습니다.
  4. 컨테이너는 런타임 데몬이 값을 정합니다. 컨테이너 안에서 /proc/1/limits를 직접 확인하십시오.
  5. fd 구성을 소켓·파일·파이프로 나눠 봅니다. CLOSE-WAIT 다발은 누수의 확정 증거입니다.
  6. 사용량이 부하와 무관하게 단조 증가하면 한계를 올려도 소용없습니다. 한계 상향은 조사할 시간을 버는 조치일 뿐 해결이 아닙니다.
  7. "Too many open files"가 나는데 fd에 여유가 있다면 inotify 인스턴스 한계를 확인하십시오. 문구가 같습니다.

Too many open files solved for good — why raising ulimit does not help

Introduction — accept4() failed (24: Too many open files)

The symptom shows up like this.

2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)
2026/07/25 14:02:11 [alert] 14022#14022: accept4() failed (24: Too many open files)

Or, in Java, like this.

java.net.SocketException: Too many open files
	at java.base/sun.nio.ch.Net.accept(Native Method)
Caused by: java.io.IOException: Too many open files

At this point almost everybody does the same thing. They run ulimit -n 65536, restart the service, and see exactly the same error. The reason is that there is not one limit but three, and the value you changed in the shell never reaches the service.

errno 24 is EMFILE. You have to read the wording precisely.

errno 24 23
# EMFILE 24 Too many open files
# ENFILE 23 Too many open files in system

If in system is attached, you are looking at a different problem. That one difference splits half the diagnosis for you.

There is not one limit but three

Three limits exist independently of one another, and hitting any single one of them stops you from opening a file.

LimitScopeerrno on excessHow to checkWhere it is set
RLIMIT_NOFILE softA single processEMFILE (24)/proc/PID/limitsulimit -n, limits.conf, systemd LimitNOFILE
RLIMIT_NOFILE hardA single processSetting refused/proc/PID/limitsSame as above, unprivileged may only lower it
fs.nr_openKernel-wideEPERM/EINVALsysctl fs.nr_open/etc/sysctl.d/
fs.file-maxWhole systemENFILE (23)cat /proc/sys/fs/file-nr/etc/sysctl.d/

Their relationship looks like this.

fs.file-max   : how many files the whole system can hold open at once (the sum over every process)
   └─ fs.nr_open : the ceiling that the RLIMIT_NOFILE of one process cannot exceed
        └─ RLIMIT_NOFILE hard : the upper bound an administrator set for that process
             └─ RLIMIT_NOFILE soft : the value actually in effect. A process can raise it up to hard on its own

Look at the current state in one go.

sysctl fs.file-max fs.nr_open
# fs.file-max = 6553600
# fs.nr_open = 1048576

cat /proc/sys/fs/file-nr
# 13984	0	6553600

The three numbers in file-nr are, in order, the count of allocated file handles, the count allocated but unused (always 0 on recent kernels), and the maximum. When the first approaches the third you have a system-wide problem, and what surfaces then is ENFILE. In the example above it is 13984 against 6.55 million, so the headroom is overwhelming. On most modern Linux systems fs.file-max is not the bottleneck. The kernel computes it automatically in proportion to the amount of memory. Even so, plenty of tuning documents tell you to raise this value first. Raising it is harmless, but the symptom stays exactly where it was.

fs.nr_open is a different story. Any ulimit -n above the default of 1048576 fails to be set at all.

ulimit -n 2000000
# bash: ulimit: open files: cannot modify limit: Operation not permitted

sudo sysctl -w fs.nr_open=2097152
# fs.nr_open = 2097152
ulimit -n 2000000   # now it succeeds

Checking the value actually in effect — /proc/PID/limits, not ulimit

This is the crux. ulimit -n shows the value of the shell you are in right now. It is not the value of the process that is failing. The two differ all the time.

ulimit -n
# 1048576

pgrep -x nginx
# 14022
# 14025

sudo grep 'Max open files' /proc/14025/limits
# Max open files            1024                 524288               files

The shell says a million while the soft limit of the nginx worker is 1024. That gap is the whole identity of the problem. From now on, when you meet this symptom, do not even look at ulimit -n — run this command first.

# every process matching a name, in one shot
for pid in $(pgrep -x nginx); do
  printf '%6s  %s\n' "$pid" "$(awk '/Max open files/ {print $4, $5}' /proc/"$pid"/limits)"
done
#  14022  1024 524288
#  14025  1024 524288

Look at how many are in use right now at the same time.

# the real number of open fds (the most accurate)
sudo ls /proc/14025/fd | wc -l
# 1021

# utilization on a single line
for pid in $(pgrep -x nginx); do
  used=$(sudo ls /proc/"$pid"/fd 2>/dev/null | wc -l)
  soft=$(awk '/Max open files/ {print $4}' /proc/"$pid"/limits)
  printf '%6s  %6s / %-8s (%d%%)\n' "$pid" "$used" "$soft" $((used * 100 / soft))
done
#  14022    1021 / 1024     (99%)
#  14025    1019 / 1024     (99%)

If you use lsof -p PID | wc -l the number comes out larger. lsof also counts the current working directory, the root directory, the executable and memory-mapped libraries, and none of those count against RLIMIT_NOFILE. When you compare against the limit, use the count from /proc/PID/fd.

Why systemd ignores your ulimit settings

This is the most common trap. If you have ever added the configuration below, restarted, and watched nothing change, this section is the answer.

cat /etc/security/limits.d/99-nofile.conf
# *    soft    nofile    1048576
# *    hard    nofile    1048576

This file is read by the PAM module pam_limits. And pam_limits works only inside a login session. SSH logins, su and login qualify. A system service that systemd starts at boot is not a login session, so it never reads this file at all.

What decides the limit of a service is systemd. The precedence is LimitNOFILE in the unit, then DefaultLimitNOFILE in /etc/systemd/system.conf if that is absent, and the systemd built-in default if that is absent too.

# the value applied to the unit (hard and soft are exposed separately)
systemctl show -p LimitNOFILE -p LimitNOFILESoft nginx.service
# LimitNOFILE=524288
# LimitNOFILESoft=1024

# the global default
systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft
# DefaultLimitNOFILE=524288
# DefaultLimitNOFILESoft=1024

Here it pays to know the default behaviour of systemd 240 and later. systemd sets the hard limit generously at 524288 while keeping the soft limit low, at 1024. The reason is compatibility. select() cannot handle fd numbers of 1024 and above, and some older programs run a loop at startup that closes every fd from 0 up to the soft limit, so if that limit is a million, starting up takes several seconds. The design intent of systemd is therefore that a program which needs more should raise it for itself.

A good number of modern runtimes really do that. The Go runtime raises the soft limit to the hard limit at startup, which is why on the same machine a Go service can be perfectly healthy while only the other services hit EMFILE.

Here is the correct way to configure it.

sudo systemctl edit nginx.service
# write the following into the editor that opens
# [Service]
# LimitNOFILE=65536

sudo systemctl daemon-reload
sudo systemctl restart nginx

Two points deserve emphasis. systemctl reload does not change rlimits. The value is applied at process creation time, so it has to be restart. And when you write a single value like LimitNOFILE=65536, both the soft and the hard limit become that value. To give them separately, use the LimitNOFILE=65536:524288 form.

Confirm that it took effect.

sudo grep 'Max open files' /proc/$(pgrep -x nginx | head -1)/limits
# Max open files            65536                65536                files

Sometimes the application has a setting of its own on top of that. nginx carries a separate directive called worker_rlimit_nofile, and that value cannot exceed the hard limit systemd handed it.

# /etc/nginx/nginx.conf
worker_rlimit_nofile 65536;
events {
    worker_connections 32768;
}

Setting worker_connections higher than worker_rlimit_nofile is meaningless. A single connection can use one client socket and one upstream socket, so for a reverse proxy it is safe to set worker_rlimit_nofile to at least twice worker_connections.

There is also a way to put out the fire without a restart.

sudo prlimit --pid 14025 --nofile=65536:65536
sudo prlimit --pid 14025 --nofile
# RESOURCE DESCRIPTION                             SOFT  HARD UNITS
# NOFILE   max number of open files               65536 65536 files

prlimit changes the limit of a running process. That said, if the application read the limit at startup and sized its internal data structures from it (the nginx connection pool does exactly that), it may have no effect. The real fix is editing the unit file and restarting.

Containers play by different rules again. The RLIMIT_NOFILE of a process inside a container comes from process.rlimits in the OCI runtime spec, and that value is decided by the container runtime daemon. Neither the shell ulimit nor the limits.conf on the host has any say.

# checking from inside the container is the only reliable method
docker exec api-server cat /proc/1/limits | grep 'Max open files'
# Max open files            1048576              1048576              files

# specify it per container
docker run --ulimit nofile=65536:65536 myimage

# the daemon-wide default
cat /etc/docker/daemon.json
# {
#   "default-ulimits": { "nofile": { "Name": "nofile", "Soft": 65536, "Hard": 65536 } }
# }

The Kubernetes pod spec has no ulimit field. To adjust it per pod you have to lower it with ulimit -n in the container entrypoint (which can only go below the hard limit), change the containerd configuration on the node, or adjust LimitNOFILE on the container runtime unit. Many distributions ship that runtime unit with LimitNOFILE=infinity, and in that case the container inherits a very large value. It looks generous and comfortable, but for a program that runs the close-every-fd loop described above it becomes the reason startup takes tens of seconds.

What is eating the fds — sockets, files and pipes, broken down

Before you raise the limit you have to see what is occupying it. The symlink targets under /proc/PID/fd tell you the kind.

sudo ls -l /proc/14025/fd | awk '{print $NF}' \
  | sed -E 's#^(socket|pipe|anon_inode):.*#\1#; s#^/.*#regular-file#' \
  | sort | uniq -c | sort -rn
#   61204 socket
#    3891 regular-file
#     412 anon_inode
#       8 pipe
#       3 /dev/null

Sixty thousand sockets means the network side. Dig further into which sockets they are.

# socket count by state
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
#   38210 ESTAB
#   21044 CLOSE-WAIT
#    2118 TIME-WAIT
#     412 LISTEN

There are twenty thousand CLOSE-WAIT sockets. This is conclusive evidence of an fd leak. CLOSE-WAIT is the state where the peer sent a FIN and closed the connection while the application on our side never called close(). The kernel waits indefinitely in that state for the application to close it. There is no timeout. Do not confuse it with TIME-WAIT. TIME-WAIT is cleaned up by the kernel on its own and consumes no fd.

Looking at which peer the connections belong to narrows down the offending code.

sudo ss -tanp state close-wait | head -5
# Recv-Q Send-Q   Local Address:Port      Peer Address:Port  Process
# 1      0        10.0.3.14:44120        10.0.9.31:6379     users:(("java",pid=21874,fd=1042))
# 1      0        10.0.3.14:44122        10.0.9.31:6379     users:(("java",pid=21874,fd=1043))

# aggregate by peer address
sudo ss -tan state close-wait | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
#   20981 10.0.9.31
#      63 10.0.9.42

They are all the same peer, Redis. That means the Redis client pool is not returning connections, and that is what decides where the code review starts.

If files are the culprit instead, count which files they are.

sudo ls -l /proc/21874/fd | awk '$NF ~ /^\// {print $NF}' \
  | sed -E 's#/[^/]+$##' | sort | uniq -c | sort -rn | head -5
#    3204 /var/lib/app/uploads/tmp
#     512 /data/kafka/logs/orders-0
#      88 /usr/lib/x86_64-linux-gnu

There are 3204 files open under /var/lib/app/uploads/tmp. That means a code path creates temporary files and never closes them.

inotify and epoll are separate limits

The anon_inode family consumes RLIMIT_NOFILE too, but it has ceilings of its own. That is why failures happen while there is still plenty of fd headroom.

inotify has three ceilings.

sysctl fs.inotify
# fs.inotify.max_queued_events = 16384
# fs.inotify.max_user_instances = 128
# fs.inotify.max_user_watches = 65536

max_user_instances is how many inotify instances (inotify_init) a user may create, and exceeding it gives EMFILE. The error text is exactly the same as the one from ulimit -n, but the cause is different. max_user_watches is how many paths a user may watch, and exceeding it gives ENOSPC, that is, "No space left on device". It has nothing to do with the disk and yet it looks like a disk error.

What matters is that both ceilings are per user. Dozens of containers running under the same UID share a single budget. The classic pattern where you add pods to a node and watch-based tools suddenly start dying comes from exactly this.

Here is how to count the current usage.

# the processes holding the instances
sudo find /proc/[0-9]*/fd -lname 'anon_inode:*inotify*' 2>/dev/null \
  | cut -d/ -f3 | sort | uniq -c | sort -rn | head -5
#      42   9931
#       8   3311
#       2   1042

# the real watch count (the number of inotify lines in fdinfo is the watch count)
sudo grep -c '^inotify' /proc/9931/fdinfo/* 2>/dev/null | grep -v ':0$' | head
# /proc/9931/fdinfo/18:31204
# /proc/9931/fdinfo/22:8102
cat /etc/sysctl.d/60-inotify.conf
# fs.inotify.max_user_watches = 524288
# fs.inotify.max_user_instances = 1024
sudo sysctl --system

epoll has a separate ceiling as well.

cat /proc/sys/fs/epoll/max_user_watches
# 3618421

This value is the number of epoll watch entries a user may register, and the kernel derives it automatically from roughly 4 percent of low memory. A single entry costs tens of bytes of kernel memory, which is why it is not unlimited. Running into this limit in the field is rare, but for a proxy handling millions of connections it belongs on the checklist.

To summarize.

ResourceCeilingUniterrno on excess
Ordinary fdRLIMIT_NOFILEProcessEMFILE (24)
System-wide filesfs.file-maxSystemENFILE (23)
inotify instancefs.inotify.max_user_instancesUserEMFILE (24)
inotify watchfs.inotify.max_user_watchesUserENOSPC (28)
epoll watchfs.epoll.max_user_watchesUserENOSPC (28)

The real cause is usually a leak — how to confirm it and how to triage

There are only two situations that raising the limit actually fixes. One is when you genuinely need that many concurrent connections, the other is when the default is absurdly low. Everything else is a leak, and raising the limit on a leak only pushes the outage a few hours into the future.

The definition of a leak is simple. If fd usage grows monotonically regardless of load, it is a leak. If it rises and falls together with load, it is an ordinary capacity problem. A few minutes of sampling is enough to tell the two apart.

PID=21874
while sleep 60; do
  printf '%s  fd=%-7s estab=%-7s close_wait=%s\n' \
    "$(date +%H:%M:%S)" \
    "$(sudo ls /proc/$PID/fd | wc -l)" \
    "$(ss -tan state established | wc -l)" \
    "$(ss -tan state close-wait | wc -l)"
done
# 14:02:11  fd=18204   estab=8102    close_wait=9931
# 14:03:11  fd=19388   estab=8044    close_wait=11102
# 14:04:11  fd=20511   estab=7981    close_wait=12290
# 14:05:11  fd=21702   estab=8120    close_wait=13401

The connection count (estab) is flat around 8000 while fd climbs by 1200 per minute. And the increase matches the increase in close_wait exactly. The socket leak is confirmed, and once ss -tanp from the previous section has pinned down the peer too, the range of code to investigate is very narrow.

To catch which code path is doing the opening, you need tracing.

# files being opened and the call stack (bcc tools)
sudo opensnoop-bpfcc -p 21874

# frequency of the system calls that open fds
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_openat /pid == 21874 && args->ret > 0/ { @[ustack] = count(); }'

# strace is expensive, so keep it short
sudo timeout 5 strace -f -e trace=openat,socket,close -p 21874 -c
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- ----------------
#  61.02    0.184203          18     10233           socket
#  38.44    0.116041          21      5488           openat
#   0.54    0.001632           4       402           close

socket was called 10233 times while close was called 402 times. The ratio alone gives you the answer.

There are three triage options, and they come in an order.

# 1) restart the process — the surest, but the service goes down
sudo systemctl restart myapp

# 2) buy time by raising only the limit with prlimit (no restart)
sudo prlimit --pid 21874 --nofile=200000:200000

# 3) force a specific fd closed — the last resort, the application may break
sudo gdb -p 21874 -batch -ex 'call (int)close(1042)'

Number 3 closes an fd while the application still believes it is valid, so you have to accept the risk of data corruption or a crash. Use it only when it is the last option left in production.

Wrapping up — three limits and one real cause

If you remember only one thing, remember this. ulimit -n is the value of the shell you are in right now and has nothing to do with the service. What you should look at is always /proc/PID/limits.

Here is the diagnostic order, compressed.

  1. Read the error text precisely. If in system is attached it is fs.file-max, otherwise it is the process limit.
  2. Read the value actually in effect from /proc/PID/limits and the real usage from ls /proc/PID/fd | wc -l. Those two alone settle immediately whether this is a limit problem or not.
  3. For a systemd service, set LimitNOFILE with systemctl edit rather than limits.conf, and restart. A reload will not change it.
  4. For containers the runtime daemon decides the value. Check /proc/1/limits directly from inside the container.
  5. Break the fd composition down into sockets, files and pipes. A pile of CLOSE-WAIT is definitive evidence of a leak.
  6. If usage grows monotonically regardless of load, raising the limit is useless. Raising a limit only buys time to investigate; it is not a fix.
  7. If you see "Too many open files" while fd headroom is fine, check the inotify instance limit. The wording is identical.