5개의 작업 트리와 5개의 node_modules: 병렬 에이전트가 Linux의 inotify 제한을 소진하는 방법
요약
여러 개의 코딩 에이전트를 사용하여 개발 서버를 부팅할 때 Linux 환경에서 'ENOSPC: System limit for number of file watchers reached' 오류가 발생했습니다. 이는 inotify 시스템의 감시(watch) 제한을 초과했기 때문입니다. 이 문제는 디렉토리 단위로 감시를 설정하고, 각 하위 디렉토리에 대한 감시는 재귀적으로 처리해야 함을 설명합니다.
핵심 포인트
- Linux 환경에서 파일 변경 감지 시 inotify 시스템의 제한에 주의해야 합니다.
- 감시는 파일 개수가 아닌 디렉토리 개수에 비례하여 증가하는 것이 효율적입니다.
- 사용자별로 할당되는 `max_user_watches` 한도를 이해하고 관리해야 합니다.
• 도서: System Design Pocket Guide: Fundamentals
• 시리즈: Pocket Guides for Developers — 에이전트, RAG, 프롬프트, 관측 가능성(observability), 이벤트, 데이터베이스 및 시스템 설계에 대한 8권의 짧은 책 — 전체 시리즈
• 제 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구와 함께 배포하는 개발자를 위한 IDE
• 저: xgabriel.com | GitHub
ENOSPC: System limit for number of file watchers reached. 5개의 코딩 에이전트 중 네 번째 에이전트가 Linux 환경에서 개발 서버를 부팅할 때 이 메시지를 출력합니다. 이는 지난 6월 Vite가 시작을 거부했을 때 Cursor 사용자 한 명이 게시한 메시지와 동일합니다.
설정상 특이한 점은 없습니다. 각 에이전트는 자체 git worktree를 가지고 있으며, 각 작업 트리는 npm ci를 실행했고, 각 에이전트는 자신의 작업을 확인하기 위해 개발 서버를 시작합니다. df -h를 실행하면 디스크가 절반 정도 비어 있습니다. ENOSPC는
inotify(7)는 Linux가 프로그램에게 파일이 변경되었다고 알리는 방식입니다. 프로그램은 경로에 감시(watch)를 추가하고 파일 디스크립터에서 이벤트를 읽습니다. 해당 man 페이지는 여기서 중요한 부분에 대해 명확하게 설명합니다: “디렉토리의 inotify 모니터링은 재귀적이지 않습니다. 디렉토리 하위의 서브디렉토리를 모니터링하려면 추가적인 감시가 생성되어야 합니다.”
따라서 프로젝트 루트 아래의 모든 변경 사항을 알고 싶어 하는 도구는 트리를 순회하며 디렉토리당 하나의 감시를 추가합니다. 파일마다 하나씩 필요하지 않습니다. 왜냐하면 디렉토리에 대한 감시는 그 안에 있는 파일들의 변경 사항을 보고하기 때문입니다. 프로젝트 규모에 따라 증가하는 숫자는 디렉토리 개수입니다.
Node의 fs.watch](https://nodejs.org/docs/latest-v24.x/api/fs.html)도 이와 같은 방식으로 작동합니다. 해당 문서에는 Linux에서 “이것은 inotify(7)를 사용한다”고 나와 있으며, v19.1.0에 Linux용으로 추가된 recursive 옵션이 동일한 하위 순회 작업을 수행해야 합니다.
규칙의 두 번째 절반은 같은 man 페이지에 있습니다. max_user_watches는 “실제 사용자 ID당 생성할 수 있는 감시의 상한선을 지정합니다.” 프로세스별이 아니라 사용자별입니다. 사용자인 당신의 다섯 개 에이전트, 그들의 다섯 개 개발 서버, 당신의 에디터, 그 언어 서버들, 그리고 감시 모드로 실행되는 모든 테스트 러너들은 모두 당신으로 실행되므로 이 하나의 풀을 공유합니다.
여기에 워크트리(worktrees)를 추가해 봅시다. Git은 워크트리 간에 히스토리를 공유하며, “HEAD, index 등 워크트리별 파일들을 제외한 모든 것”을 공유합니다. node_modules와 같은 무시된 폴더는 전혀 공유되지 않으므로, 자체 종속성을 설치하는 모든 워크트리는 트리의 자신만의 사본을 갖게 됩니다. 하드 링크(hard links)가 해결책이 되지 못합니다: link(2)는 “oldpath가 디렉토리인 경우” EPERM 오류를 발생시키므로, 각 사본은 신선한 디렉토리 세트입니다. 다섯 개의 워크트리는 감시하는 모든 항목에 대해 대략 다섯 배의 디렉토리를 의미합니다.

모든 worktree는 자체 종속성 트리(dependency tree)를 가지며, 그 안의 모든 디렉토리는 사용자별 풀에서 가져온 감시(watch)입니다.
설정된 제한 확인하기
inotify가 사용되던 대부분의 기간 동안 기본값은 8192였습니다. Waiman Long이 수행한 커널 변경은 이를 메모리 크기로 대체했습니다. 이 변경 사항은 2020년 12월에 fsnotify의 v5.11-rc1용 풀과 함께 병합되었고, Linux 5.11에서 출시되었습니다. 커밋 메시지에는 8192가 inotify가 2005년에 도입된 이래 기본값이었다고 언급되어 있습니다. 오늘날의 계산은 fs/notify/inotify/inotify_user.c에서 가져온 것입니다:
watches_max = (((si.totalram - si.totalhigh) / 100)
<< PAGE_SHIFT) / INOTIFY_WATCH_COST;
watches_max = clamp(watches_max, 8192UL, 1048576UL);
이는 낮은 메모리(low memory)의 1%를 감시당 하나의 추정 비용(inotify 마크 크기 및 두 개의 inode 크기)으로 나눈 값이며, 8192와 1,048,576 사이로 제한됩니다. 커밋 메시지에 따르면 128GB 이상의 메모리를 가진 64비트 시스템은 최대치를 얻을 가능성이 높습니다. 그 이하에서는 값이 RAM과 선형적으로 비례하므로, 16GB 장치는 약 131,000 정도로 상한선의 8분의 1 정도에 위치하게 됩니다.
커널 기본값 외에도 배포판(distro)이나 패키지들이 자체 설정을 하는 경우가 많습니다. 위 Cursor 보고서는 65,536 메모리 장치에서 나온 것입니다. VS Code의 Linux 문서는 524,288을 제안하며
두 개의 숫자가 다섯 개의 worktree를 사용할 수 있는지 여부를 결정합니다. 첫 번째는 순진한 재귀적 워처(naive recursive watcher)가 커버할 디렉터리 개수이고, 두 번째는 현재 프로세스가 보유하고 있는 워치(watch) 개수입니다. 둘 다 몇 줄의 bash 스크립트로 확인할 수 있습니다.
첫 번째 숫자는 현재 저장소의 모든 worktree를 탐색합니다. git worktree list --porcelain은 worktree당 하나의 worktree <경로> 라인을 출력합니다:
#!/usr/bin/env bash
limit=$(cat /proc/sys/fs/inotify/max_user_watches)
total=0
...
각 카운트에서 의존성(dependencies)이 얼마나 차지하는지 확인하려면 "$wt"를 "$wt/node_modules"로 교체하십시오. 메인 worktree의 카운트는 제외할 항목이 없는 워처의 경우 .git 디렉터리도 포함한다는 점에 유의해야 합니다. 이 총합을 최대치(ceiling)로 간주하면 됩니다: 잘 작동하는 도구들은 node_modules와 빌드 출력물은 건너뛰기 때문에 훨씬 적게 사용합니다.
두 번째 숫자는 현재 사용 중인 워치의 개수입니다. 각 inotify watch는 프로세스의 fdinfo에서 하나의 inotify wd: 라인으로 나타나며, 이는 proc_pid_fdinfo(5)에 문서화되어 있습니다. 이 방법은 Cursor 리포터가 두 가지 가장 큰 소비자(biggest consumers)를 찾는 데 사용했던 방식과 동일합니다:
#!/usr/bin/env bash
for p in /proc/[0-9]*; do
n=$(cat "$p"/fdinfo/* 2>/dev/null | grep -c '^inotify wd:')
...
루트 권한 없이 자신의 프로세스에 대해서만 fdinfo를 읽을 수 있습니다. 여기서는 괜찮습니다. 왜냐하면 이 제한은 사용자별(per user)이며, 워치를 많이 사용하는 주체가 바로 사용자의 프로세스들이기 때문입니다. 저는 특정 프로젝트에서 이것들을 실행해 본 적이 없으므로 여기에 숫자는 없습니다. worktree를 추가하기 전과 후에 직접 테스트하여 확인하십시오.
Mac에서 이 한계에 도달하지 않는 이유
동일한 다섯 가지 에이전트를 macOS에서 실행하면 개발 서버가 정상적으로 작동합니다. Node의 문서는 한 줄로 그 차이를 설명합니다: macOS에서는 fs.watch가 "파일에는 kqueue(2)를, 디렉터리에는 FSEvents를 사용"하기 때문입니다.
Apple의 File System Events 개요는 해당 모델을 설명합니다. 사용자는 디렉터리 계층 구조에 대해 한 번 등록하면 되며, "파일 시스템 이벤트 데몬이 모니터링된 디렉터리 계층 구조 내의 어떤 파일이라도 변경될 때마다 알림을 게시합니다." 하나의 등록으로 전체 트리를 커버하므로, 그 아래에 있는 디렉터리 개수가 사용자에게 청구되는 커널 워치(kernel watches) 횟수로 계산되지 않습니다.
이 방식의 단점은 정밀도입니다. 같은 페이지에는 "알림의 세분성은 디렉터리 수준입니다. 디렉터리 내에서 무언가 변경되었다는 것만 알려줄 뿐, 무엇이 변경되었는지는 알려주지 않으며", 또한 데몬이 변화의 급증(bursts of changes)을 병합(coalesces)한다고 명시되어 있습니다. macOS의 도구들은 무슨 일이 일어났는지 알아내기 위해 디렉터리를 재검사합니다. 반면 Linux에서는 정확한 파일별 이벤트를 얻고, 디렉터리마다 비용을 지불해야 합니다.

FSEvents는 하나의 등록으로 계층 구조를 감시합니다. inotify는 그 안의 모든 디렉터리에 대한 워치(watch)가 필요합니다.
가장 저렴한 해결책들
아무도 편집하지 않는 것을 감시하는 것을 멈추세요. 대부분의 도구들은 기본적으로 종속성 폴더를 제외합니다. Vite는 "기본적으로 .git/, node_modules/, test-results/, 그리고 Vite의 cacheDir 및 build.outDir 디렉터리를 건너뜁니다." VS Code의 기본 files.watcherExclude는 "node_modules와 .git 아래의 일부 폴더를 제외합니다." webpack watch 문서는 워처에 대한 한 줄 코드를 보여줍니다:
export default {
watchOptions: {
ignored: /node_modules/,
...
Cursor 보고서에 따르면, node_modules, target, dist, build, .git/objects, .cargo 및 몇 가지 프로젝트별 폴더에 대해 files.watcherExclude 항목을 추가하자 동일한 작업 공간에서 총 감시(watch) 횟수가 약 65,532개에서 약 193개로 줄어들었습니다. 각 에이전트가 시작하는 개발 서버를 포함하여 설정된 모든 감시기를 확인하세요. 제외 항목 없는 하나의 도구가 다른 모든 것을 위한 풀을 고갈시킬 수 있습니다.
필요할 때 상한선을 높이세요. 카운터가 작업에 실제로 더 많은 감시가 필요하다고 말한다면, sysctl 드롭인에서 값을 설정하세요. VS Code와 webpack 모두 524,288을 제안합니다:
echo 'fs.inotify.max_user_watches=524288' \
| sudo tee /etc/sysctl.d/90-inotify.conf
sudo sysctl --system
VS Code가 감시당 1,080바이트라는 수치를 기억하세요. 더 높은 제한은 무언가가 실제로 사용할 경우 커널 메모리에 대한 약속이므로, 작은 VM에서는 더 낮은 숫자를 선택하고 먼저 제외 항목을 처리하는 것이 좋습니다.
Hermes 2.2.9에서 변경된 사항
Hermes IDE는 Claude Code, Codex 및 기타 에이전트를 나란히 실행하며, 각 작업은 자체 브랜치와 작업 공간(worktree)을 가질 수 있습니다. 이는 하나의 장치에 많은 작업 공간을 배치하고, 각각의 프로젝트가 가진 모든 의존성 및 빌드 폴더를 갖게 합니다. 2.2.9 이전에는 Hermes 자체가 이 풀을 사용하는 소비자 중 하나였습니다.
2026년 10월 12일에 게시된 2.2.9 릴리스에서는 다음과 같이 언급합니다: "Hermes는 작업 공간에 대규모 의존성 또는 빌드 폴더가 포함되어 있을 때 더 이상 시스템의 파일 감시 제한을 늦추거나 소모하지 않습니다." 노트에는 수정 방법이 나와 있지 않습니다. 사용자별 제한 기준으로 볼 때, 결과가 중요합니다. Hermes가 더 이상 점유하지 않는 감시는 에이전트의 개발 서버 및 테스트 러너에 여전히 사용할 수 있습니다.
같은 릴리스에서는 세션이 종료되었을 때 삭제할 수 없었던 작업 공간을 다음 실행 시 재시도하여 제거하는 기능도 추가되었습니다. 남은 작업 공간이 적다는 것은 어떤 것이든 탐색할 node_modules 복사본이 적다는 것을 의미합니다.
Hermes IDE를 구축했으니 이 부분을 염두에 두고 읽어주세요. 무료이며 BSL 1.1 하에 소스 코드가 공개되어 있고 macOS, Windows, Linux에서 실행됩니다. 어떤 도구를 사용하든 병렬 에이전트를 실행할 때, fdinfo 스크립트가 풀(pool)을 사용하는 프로세스를 이름 지어줄 것입니다.
유용했다면
사용자별 감시 제한(watch limit)은 많은 프로세스가 끌어다 쓰는 공유 예산이며 아무도 소유하지 않습니다. 수요를 줄일지 아니면 상한선을 높일지 결정하기 전에 누가 이 예산을 사용하는지 측정하는 것이 제가 System Design Pocket Guide: Fundamentals에서 시스템 규모로 다루는 습관입니다. 이것은 Pocket Guides for Developers 시리즈의 일부입니다. 에이전트, RAG, 프롬프트, 관측 가능성(observability), 이벤트, 데이터베이스 및 시스템 설계에 대한 8권의 짧은 책들입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기