Классическая ситуация знакома большинству разработчиков на Linux: после клонирования очередного монорепозитория IDE или бандлер внезапно падает с ошибкой "ENOSPC: System limit for number of file watchers reached", хотя на диске свободно ещё сотни гигабайт. Сообщение про нехватку места вводит в заблуждение, поскольку проблема не имеет никакого отношения к файловой системе. Ядро исчерпало квоту на отслеживание файлов через подсистему inotify, и пока лимиты не подняты, никакой npm install или make watcher работать не будет. Ниже разобрано, почему ошибка выглядит именно так, какие три параметра ядра за неё отвечают, как найти потребителей watches и как поднять лимит навсегда, не перезагружая машину.
Почему ошибка ENOSPC от inotify_add_watch не означает переполнение диска
Подсистема inotify существует в ядре Linux с версии 2.6.13 и служит для уведомления пользовательских программ об изменениях файлов и каталогов. Приложение вызывает inotify_init(), получает файловый дескриптор экземпляра, а затем через inotify_add_watch() подписывается на события конкретного файла или каталога: создание, удаление, модификацию, переименование. Проблема в том, что watching работает на уровне inode и не поддерживает рекурсию. Чтобы отследить дерево из ста тысяч файлов, наблюдателю нужно создать watch на каждый каталог дерева, а нередко и на каждый файл.
Когда приложение вызывает inotify_add_watch() и лимит исчерпан, ядро возвращает код ошибки ENOSPC. Исторически старшие биты этого кода совпадают с ситуацией переполнения диска, поэтому многие программные стеки честно пишут "недостаточно места на устройстве". Администратор смотрит df -h, видит 70% свободного места и теряет час впустую. Правильный первый шаг при таком симптоме звучит просто: проверить строку ошибки целиком. Если рядом с ENOSPC упоминается watcher, inotify или "file watchers", это квота ядра, а не квота диска.
Типичные виновники в окружении разработчика известны наперечёт: WebStorm, VS Code и другие IDE, отслеживающие дерево проекта включая node_modules; webpack --watch, chokidar, nodemon, jest --watch; systemd-path units; балансировщики с авторелоадом конфигов. Один средний проект с node_modules легко содержит 150-300 тысяч файлов. Три таких проекта, открытых одновременно, сметают дефолтный лимит без шансов.
Три лимита в /proc/sys/fs/inotify и их реальные значения по умолчанию
Ядро хранит настройки inotify в трёх файлах псевдофайловой системы /proc. Посмотреть текущие значения на живой машине можно одной командой.
# Выводим все три параметра inotify сразу, по одному на строку
ls /proc/sys/fs/inotify/*
max_queued_events max_user_instances max_user_watches
# Читаем каждый лимит
cat /proc/sys/fs/inotify/max_user_watches
65536
cat /proc/sys/fs/inotify/max_user_instances
128
cat /proc/sys/fs/inotify/max_queued_events
16384
Разбор по строкам. Первое число, max_user_watches со значением 65536, задаёт максимальное суммарное количество watches, которые может создать один пользователь на всей системе. Именно его чаще всего не хватает, и именно его советуют поднимать все баг-трекеры IDE. Второе, max_user_instances равное 128, ограничивает число отдельных экземпляров inotify (файловых дескрипторов от inotify_init или inotify_init1) на пользователя. Третье, max_queued_events со значением 16384, это глубина очереди событий для одного экземпляра; при переполнении очереди процесс получает IN_Q_OVERFLOW и обязан перечитать всё дерево сам.
Каждый watch занимает память ядра точно, но недорого: сами структуры inotify в нагруженном сценарии полумиллиона watches съедают порядка 500-800 МБ оперативной памяти, что для рабочей станции с 32-64 ГБ не критично. Именно поэтому поднятие лимита считается безопасной и рутинной операцией. Волшебное число 524288, встречающееся в тысячах issue на GitHub, выбрано эмпирически: оно покрывает несколько крупных проектов одновременно при разумном расходе памяти.
Отдельная тонкость: лимиты max_user_watches и max_user_instances привязаны к реальному UID пользователя, а не к процессу. Запущенный под другим пользователем фоновый сервис (например, сборочный агент CI) живёт со своей собственной квотой и не мешает разработчику.
Пошаговая диагностика: кто именно съел все watches
Прежде чем крутить ручки sysctl, полезно понять, кто потребляет ресурс. Быстрая сводка расхода watches по процессам собирается одной строкой без установки дополнительных пакетов.
# Проходим по всем процессам, находим fdinfo для дескрипторов inotify и считаем watches
for f in /proc/[0-9]*/fdinfo/*; do
grep -q inotify "$f" 2>/dev/null && echo "$f $(grep -c "^inotify" "$f")"
done | awk '{split($1,a,"/"); cnt[a[3]]+=$2} END {for (p in cnt) print cnt[p], p}' | sort -rn | head -10
Разбор: for проходит по всем файлам fdinfo всех числовых PID в /proc; grep -q inotify оставляет только дескрипторы типа inotify; grep -c "^inotify" считает строки подписок внутри каждого дескриптора; awk складывает счётчики по PID; sort -rn сортирует по убыванию, head оставляет десятку лидеров. Сопоставив PID с результатом ps -p PID -o pid,comm,args, администратор за секунды узнает виновника: чаще всего там code, jedi или какой-нибудь заброшенный webpack из прошлого года.
Когда нужно понять, а какие каталоги отслеживаются, помогает пакет inotify-tools. Утилита inotifywatch запускает временный наблюдатель и показывает частоту событий, а inotifywait блокируется до наступления конкретного события.
# Считаем события в каталоге проекта в течение 60 секунд, рекурсивно
inotifywatch -t 60 -r /var/www/project
Establishing watches...
Finished establishing watches, now collecting statistics.
total modify create delete filename
13482 12004 1490 8 /var/www/project/node_modules/.cache/
Такой вывод моментально объясняет картину: кэш-сборщик постоянно пишет в node_modules/.cache, генерируя тысячи изменений в минуту. Логичное решение тут не поднимать лимиты в бесконечность, а исключить шумные каталоги из наблюдения. В VS Code для этого служит настройка files.watcherExclude, в WebStorm раздел excluded directories, у chokidar опция ignored. Одно корректное исключение иногда экономит половину всех watches.
Проверить симптом "приложениям не хватает экземпляров" можно так: если в логах вместо ENOSPC встречается EMFILE на вызове inotify_init, виноват max_user_instances, и крутить нужно вторую ручку, а не первую.
Как выглядит ошибка в реальных логах и что делать в первые пять минут
Симптом прилетает из разных инструментов в разной обёртке. TypeScript compiler в режиме watch выдаёт "Error: ENOSPC: System limit for number of file watchers reached". Angular CLI выводит "inotify_add_watch failed: No space left on device". Rails-разработчик видит похожее от listen gem. Признак общий: фраза ENOSPC рядом со словом watcher или watch и нормальный df.
# Классический вывод упавшего webpack watcher из лога CI
[webpack-dev-middleware] Error: ENOSPC: System limit for number of file watchers reached, watch '/srv/app/node_modules/react/index.js'
at FSWatcher.<anonymous> (/srv/app/node_modules/chokidar/index.js:598:20)
Разбор вывода: первая строка сообщает код ENOSPC и путь, на котором сорвалась подписка; вторая указывает, что стек идёт через chokidar, а значит речь именно о ядерной квоте inotify. Никаких сообщений про диск. Первое действие администратора простое: сверить текущее значение.
# Смотрим актуальную квоту watches для пользователя
cat /proc/sys/fs/inotify/max_user_watches
65536
# Прикинуть текущий расход по всем процессам можно быстрой сводкой
find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | cut -d/ -f3 | sort -u | wc -l
14
Здесь wc -l показал количество процессов, открывших inotify-дескрипторы, и уже эта цифра намекает на масштаб проблемы. Если лимит 65536, а проект в IDE весит 120 тысяч файлов, добавка в лице второго проекта гарантированно роняла бы watcher ещё в прошлый спринт, и вопрос лишь в том, какой инструмент первым упрётся.
Постоянное увеличение лимитов через sysctl.d и его проверка без перезагрузки
Временное изменение через echo в /proc живёт до первой перезагрузки, поэтому на рабочих станциях и CI-агентах лимиты фиксируются в /etc/sysctl.d. Порядок действий стандартный и занимает минуту:
- Создаём отдельный файл /etc/sysctl.d/99-inotify.conf с тремя строками: fs.inotify.max_user_watches=524288, fs.inotify.max_user_instances=1024 и fs.inotify.max_queued_events=32768;
- Применяем настройки без перезагрузки командой sysctl --system или точечно sysctl -p /etc/sysctl.d/99-inotify.conf;
- Проверяем результат через cat /proc/sys/fs/inotify/max_user_watches и убеждаемся в появлении 524288;
- Перезапускаем IDE или watcher, чтобы приложение заново подписалось на события и прошло прежнюю точку отказа;
- Добавляем файл в систему контроля версий конфигураций или в provisioning-скрипты, чтобы не ловить ту же ошибку после переустановки системы.
Почему файл назван 99-inotify.conf: пронумерованные файлы в sysctl.d применяются по алфавиту, и больший номер гарантирует, что локальные правки перекроют любые системные дефолты, лежащие в /usr/lib/sysctl.d.
# Применяем конфиг и сразу сверяем фактические значения ядра
sysctl -p /etc/sysctl.d/99-inotify.conf
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 1024
fs.inotify.max_queued_events = 32768
Вывод показывает, что все три параметра подхвачены. Если вместо нового значения осталось старое, ищите опечатку в имени ключа: параметр с ошибкой просто отбрасывается без диагностики. Проверить отсутствие опечаток можно командой sysctl fs.inotify, которая выводит всю ветку параметров.
Числа выше подобраны с запасом. 524288 watches хватает подмонорепозитории на сотни тысяч файлов плюс IDE, 1024 instances закрывает паранойю крупных CI-раннеров с десятками параллельных джобов, а 32768 queued events спасает от IN_Q_OVERFLOW при массовых операциях вроде git checkout ветки с тысячами затронутых файлов, когда за секунду рождаются десятки тысяч событий.
Типичные причины повторного упирания в лимит и профилактика на будущее
Даже после поднятия лимита практика показывает: спустя полгода кто-то снова пишет в чат о вылетевшем watcher. Причины повторяются из раза в раз. Первый кандидат на пересмотр это процессы-зомби из мира наблюдателей: IDE и фоновые сервисы не всегда вызывают inotify_rm_watch и close для дескриптора при аварийном завершении, и kubectl exec, docker exec или tmux-сессии копят висящие экземпляры. Приведённый выше awk-скрипт расхода по PID в сочетании с регулярным ps выявляет их на раз.
Вторая категория причин связана с контейнерами. inotify limits являются глобальными для ядра и делятся между всеми контейнерами на хосте, потому что контейнеры живут на общем ядре. Команда разработчиков, запустившая десяток docker compose стеков с volume bind mounts и watchers внутри, потребляет квоту хоста суммарно. Профилактика здесь двойная: лимиты поднимаются на хосте, а чрезмерно шумные проекты выносятся в виртуальную машину или в devcontainer с отдельной квотой.
Третий сценарий это монорепозитории с гигантским node_modules, когда разумнее перейти на инструменты с другой моделью наблюдения. Проект watchman отслеживает дерево через один watcher и раздаёт события всем желающим по сокету, кратно снижая число watches на машине. webpack 5 умеет следовать настройке watchOptions.ignored с массивами вроде "/node_modules/", срезая подписку в десятки раз. Nodemon принимает флаг --ignore с тем же эффектом. Правильная стратегия не гонка за миллионом watches, а комбинация точечных исключений и разумного потолка в 524288.
Отдельно стоит держать в голове max_queued_events. Если в логах приложения появляется строка "event queue overflow" или маска IN_Q_OVERFLOW, администратор имеет дело не с нехваткой watches, а с тем, что события генерируются быстрее, чем процесс успевает их читать. Здесь поднимают третий параметр до 32768 или 65536 и параллельно смотрят, кто штормит файловую систему: часто это ротация логов или инкрементальный бэкап, бегущий прямо в наблюдаемое дерево. Один crontab-джоб, копирующий тысячи файлов внутрь проекта каждые полчаса, роняет самый здоровый watcher.
Финальный совет из практики: добавьте проверку трёх значений /proc/sys/fs/inotify в onboarding-скрипт для новых машин команды. Одна строка в ansible playbook или докер-образе CI-раннера снимает целый класс вопросов "почему у меня не работает hot reload", и вопросы прекратятся.