Skip to content

EMFILE crash in --serve mode when content is symlinked #2335

@koromiko

Description

@koromiko

Description

Running npx quartz build --serve crashes with EMFILE: too many open files when the content/ directory is a symlink to a folder outside quartz/.

The root cause is in quartz/cli/handlers.js — the globby call at line 471 uses **/*.ts, **/*.tsx, **/*.scss globs without restricting symlink traversal. When content/ is a symlink (e.g. content → ../vault), globby follows it, resolves back to the parent directory containing quartz/, and recurses infinitely:

quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/...

This expands to 50,000+ paths, exhausting the OS file descriptor limit regardless of ulimit settings.

Note: The build itself (npx quartz build) succeeds without issues — only the --serve file watcher is affected.

Steps to Reproduce

  1. Set up Quartz with a symlinked content directory:

    ln -s /path/to/my/vault quartz/content

    Where the vault is a sibling or ancestor-level directory of quartz/.

  2. Run the dev server:

    npx quartz build --serve
  3. The site builds and serves successfully, but the file watcher immediately crashes.

Error Log

Parsed 247 Markdown files in 2s
Filtered out 245 files in 250μs
Emitting files
Emitted 393 files to `public` in 732ms
Done processing 247 files in 3s
Started a Quartz server listening at http://localhost:8080
hint: exit with ctrl+c
Error: EMFILE: too many open files, watch 'quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/quartz/content/quartz/node_modules/mathjax-full/ts/output/common/Notation.ts'
    at FSWatcher.<computed> (node:internal/fs/watchers:254:19)
    at watch (node:fs:2551:36)
    at createFsWatchInstance (file:///quartz/node_modules/chokidar/handler.js:126:16)
    at setFsWatchListener (file:///quartz/node_modules/chokidar/handler.js:171:19)
    at NodeFsHandler._watchWithNodeFs (file:///quartz/node_modules/chokidar/handler.js:327:22)
    at NodeFsHandler._handleFile (file:///quartz/node_modules/chokidar/handler.js:393:29)
    at NodeFsHandler._addToNodeFs (file:///quartz/node_modules/chokidar/handler.js:618:31)
    at file:///quartz/node_modules/chokidar/index.js:367:25
    at async Promise.all (index 50311)
fatal error: all goroutines are asleep - deadlock!

Suggested Fix

In quartz/cli/handlers.js, add followSymbolicLinks: false and ignore the content/ directory in the globby options, since the watcher only needs to watch source code and styles, not content files (content changes are already handled separately by the build pipeline):

     const paths = await globby([
       "**/*.ts",
       "quartz/cli/*.js",
       "quartz/static/**/*",
       "**/*.tsx",
       "**/*.scss",
       "package.json",
-    ])
+    ], { followSymbolicLinks: false, ignore: ["content/**"] })

Environment

  • Quartz version: 4.5.2
  • Node.js version: v24.13.0
  • OS: macOS (Darwin 25.2.0)
  • Content setup: quartz/content symlinked to sibling vault/ directory

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions