codex CLI accumulates ~650,000 open file descriptors (kqueue EVFILT_VNODE watchers) on FreeBSD, wedging unrelated processes

Open 💬 3 comments Opened Jul 28, 2026 by delleceste

What version of Codex CLI is running?

0.145.0

What subscription do you have?

plus

Which model were you using?

codex, any

What platform is your computer?

FreeBSD (but the problem should involve an upstream fix)

What terminal emulator and version are you using (if applicable)?

konsole

Codex doctor report

{
  "schemaVersion": 1,
  "generatedAt": "1785242077s since unix epoch",
  "overallStatus": "ok",
  "codexVersion": "0.145.0",
  "checks": {
    "app_server.status": {
      "id": "app_server.status",
      "category": "app-server",
      "status": "ok",
      "summary": "background server is not running",
      "details": {
        "control socket": "/home/giacomo/.codex/app-server-control/app-server-control.sock",
        "daemon state dir": "/home/giacomo/.codex/app-server-daemon",
        "mode": "ephemeral",
        "pid file": "/home/giacomo/.codex/app-server-daemon/app-server.pid (missing)",
        "settings": "/home/giacomo/.codex/app-server-daemon/settings.json (missing)",
        "status": "not running",
        "update-loop pid file": "/home/giacomo/.codex/app-server-daemon/app-server-updater.pid (missing)"
      },
      "remediation": null,
      "durationMs": 0
    },
    "auth.credentials": {
      "id": "auth.credentials",
      "category": "auth",
      "status": "ok",
      "summary": "auth is configured",
      "details": {
        "auth file": "/home/giacomo/.codex/auth.json",
        "auth storage mode": "File",
        "stored API key": "false",
        "stored ChatGPT tokens": "true",
        "stored agent identity": "false",
        "stored auth mode": "chatgpt"
      },
      "remediation": null,
      "durationMs": 0
    },
    "config.load": {
      "id": "config.load",
      "category": "config",
      "status": "ok",
      "summary": "config loaded",
      "details": {
        "CODEX_HOME": "/home/giacomo/.codex",
        "config.toml": "/home/giacomo/.codex/config.toml",
        "config.toml parse": "ok",
        "cwd": "/home/giacomo/devel/cs/gui/tcbuf-plot",
        "enabled feature flags": "shell_tool, unified_exec, shell_snapshot, code_mode_host, terminal_resize_reflow, sqlite, hooks, enable_request_compression, multi_agent, apps, tool_search_always_defer_mcp_tools, tool_suggest, plugins, in_app_browser, browser_use, browser_use_full_cdp_access, browser_use_external, computer_use, remote_plugin, plugin_sharing, image_generation, resize_all_images, skill_mcp_dependency_install, skill_search, mentions_v2, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, auth_elicitation, personality, fast_mode, tui_app_server, remote_compaction_v2, workspace_dependencies",
        "feature flag overrides": "none",
        "feature flags enabled": "36",
        "log dir": "/home/giacomo/.codex/log",
        "mcp servers": "0",
        "model": "<default>",
        "model provider": "openai",
        "sqlite home": "/home/giacomo/.codex"
      },
      "remediation": null,
      "durationMs": 0
    },
    "git.environment": {
      "id": "git.environment",
      "category": "git",
      "status": "ok",
      "summary": "git version 2.55.0",
      "details": {
        ".git entry": "directory",
        "PATH git #1": "/usr/local/bin/git",
        "PATH git entries": "1",
        "git branch": "layout-compact",
        "git build options": "git version 2.55.0; cpu: amd64; no commit associated with this build; sizeof-long: 8; sizeof-size_t: 8; shell-path: /bin/sh; rust: disabled; gettext: enabled; libcurl: 8.21.0; OpenSSL: OpenSSL 3.5.6 7 Apr 2026; zlib: 1.3.1; SHA-1: SHA1_DC; SHA-256: SHA256_BLK; default-ref-format: files; default-hash: sha1",
        "git exec path": "/usr/local/libexec/git-core",
        "git version": "git version 2.55.0",
        "repo detected": "true",
        "repo root": "/home/giacomo/devel/cs/gui/tcbuf-plot",
        "selected git": "/usr/local/bin/git"
      },
      "remediation": null,
      "durationMs": 21
    },
    "installation": {
      "id": "installation",
      "category": "install",
      "status": "ok",
      "summary": "installation looks consistent",
      "details": {
        "PATH codex #1": "/usr/local/bin/codex",
        "current executable": "/usr/local/bin/codex",
        "install context": "other",
        "managed by bun": "false",
        "managed by npm": "false",
        "managed by pnpm": "false",
        "managed package root": "not set"
      },
      "remediation": null,
      "durationMs": 0
    },
    "mcp.config": {
      "id": "mcp.config",
      "category": "mcp",
      "status": "ok",
      "summary": "no MCP servers configured",
      "details": {},
      "remediation": null,
      "durationMs": 0
    },
    "network.env": {
      "id": "network.env",
      "category": "network",
      "status": "ok",
      "summary": "network-related environment looks readable",
      "details": {
        "proxy env vars": "none"
      },
      "remediation": null,
      "durationMs": 0
    },
    "network.provider_reachability": {
      "id": "network.provider_reachability",
      "category": "reachability",
      "status": "ok",
      "summary": "active provider endpoints are reachable over HTTP",
      "details": {
        "ChatGPT base URL": "https://chatgpt.com/backend-api/ reachable (HTTP 404)",
        "reachability mode": "ChatGPT auth"
      },
      "remediation": null,
      "durationMs": 654
    },
    "network.websocket_reachability": {
      "id": "network.websocket_reachability",
      "category": "websocket",
      "status": "ok",
      "summary": "Responses WebSocket handshake succeeded",
      "details": {
        "DNS": "2 IPv4, 2 IPv6, first IPv4",
        "auth mode": "chatgpt",
        "connect timeout": "15000 ms",
        "endpoint": "wss://chatgpt.com/backend-api/<redacted>",
        "handshake result": "HTTP 101 Switching Protocols",
        "model provider": "openai",
        "models etag present": "true",
        "provider name": "OpenAI",
        "proxy env vars": "none",
        "reasoning header": "false",
        "server model present": "false",
        "supports websockets": "true",
        "wire API": "responses"
      },
      "remediation": null,
      "durationMs": 2134
    },
    "runtime.provenance": {
      "id": "runtime.provenance",
      "category": "runtime",
      "status": "ok",
      "summary": "running local build on freebsd-x86_64",
      "details": {
        "commit": "unknown",
        "current executable": "/usr/local/bin/codex",
        "install method": "other",
        "platform": "freebsd-x86_64",
        "version": "0.145.0"
      },
      "remediation": null,
      "durationMs": 0
    },
    "runtime.search": {
      "id": "runtime.search",
      "category": "search",
      "status": "ok",
      "summary": "search is OK (system)",
      "details": {
        "search command": "rg",
        "search command readiness": "ripgrep 15.1.0",
        "search provider": "system"
      },
      "remediation": null,
      "durationMs": 1
    },
    "sandbox.helpers": {
      "id": "sandbox.helpers",
      "category": "sandbox",
      "status": "ok",
      "summary": "sandbox configuration is readable",
      "details": {
        "approval policy": "OnRequest",
        "codex-linux-sandbox helper": "none",
        "execve wrapper helper": "/home/giacomo/.codex/tmp/arg0/codex-arg0nuV8Sh/codex-execve-wrapper",
        "filesystem sandbox": "restricted",
        "network sandbox": "restricted"
      },
      "remediation": null,
      "durationMs": 0
    },
    "state.paths": {
      "id": "state.paths",
      "category": "state",
      "status": "ok",
      "summary": "state paths and databases are inspectable",
      "details": {
        "CODEX_HOME": "/home/giacomo/.codex (dir)",
        "active rollout files": "0 files, 0 total bytes, 0 average bytes",
        "archived rollout files": "0 files, 0 total bytes, 0 average bytes",
        "goals DB": "/home/giacomo/.codex/goals_1.sqlite (file)",
        "goals DB integrity": "ok",
        "log DB": "/home/giacomo/.codex/logs_2.sqlite (file)",
        "log DB integrity": "ok",
        "log dir": "/home/giacomo/.codex/log (missing)",
        "memories DB": "/home/giacomo/.codex/memories_1.sqlite (file)",
        "memories DB integrity": "ok",
        "sqlite home": "/home/giacomo/.codex (dir)",
        "state DB": "/home/giacomo/.codex/state_5.sqlite (file)",
        "state DB integrity": "ok",
        "thread history DB": "/home/giacomo/.codex/thread_history_1.sqlite (missing)",
        "thread history DB integrity": "skipped (missing)"
      },
      "remediation": null,
      "durationMs": 23
    },
    "state.rollout_db_parity": {
      "id": "state.rollout_db_parity",
      "category": "threads",
      "status": "ok",
      "summary": "rollout files and state DB thread inventory agree",
      "details": {
        "default model provider": "openai",
        "rollout DB active files": "0",
        "rollout DB active rows": "0",
        "rollout DB archive mismatches": "0",
        "rollout DB archived files": "0",
        "rollout DB archived rows": "0",
        "rollout DB duplicate DB paths": "0",
        "rollout DB duplicate rollout thread ids": "0",
        "rollout DB malformed file names": "0",
        "rollout DB missing active rows": "0",
        "rollout DB missing archived rows": "0",
        "rollout DB model providers": "none",
        "rollout DB rows": "0",
        "rollout DB scan cap reached": "false",
        "rollout DB scan errors": "0",
        "rollout DB sources": "none",
        "rollout DB stale rows": "0"
      },
      "remediation": null,
      "durationMs": 20
    },
    "system.environment": {
      "id": "system.environment",
      "category": "system",
      "status": "ok",
      "summary": "OS language C",
      "details": {
        "EDITOR": "set",
        "LANG": "C.UTF-8",
        "PAGER": "set",
        "VISUAL": "not set",
        "os": "FreeBSD 15.0-RELEASE-p5 [64-bit]",
        "os language": "C",
        "os type": "FreeBSD",
        "os version": "15.0-RELEASE-p5"
      },
      "remediation": null,
      "durationMs": 1
    },
    "terminal.env": {
      "id": "terminal.env",
      "category": "terminal",
      "status": "ok",
      "summary": "terminal metadata was detected",
      "details": {
        "COLORFGBG": "15;0",
        "COLORTERM": "truecolor",
        "DISPLAY": "present",
        "color output": "enabled",
        "effective locale": "C.UTF-8",
        "stderr is terminal": "true",
        "stdin is terminal": "true",
        "stdout is terminal": "true",
        "terminal": "Konsole",
        "terminal size": "179x52",
        "terminal version": "260403"
      },
      "remediation": null,
      "durationMs": 0
    },
    "terminal.title": {
      "id": "terminal.title",
      "category": "title",
      "status": "ok",
      "summary": "terminal title default",
      "details": {
        "terminal title activity": "true",
        "terminal title items": "activity, project-name",
        "terminal title project source": "git repo root",
        "terminal title project value": "tcbuf-plot",
        "terminal title source": "default"
      },
      "remediation": null,
      "durationMs": 0
    },
    "updates.status": {
      "id": "updates.status",
      "category": "updates",
      "status": "ok",
      "summary": "update configuration is locally consistent",
      "details": {
        "cached latest version": "0.145.0",
        "check for update on startup": "true",
        "last checked at": "2026-07-28T12:31:38.000121954Z",
        "latest version": "0.145.0",
        "latest version status": "current version is not older",
        "update action": "manual or unknown",
        "version cache": "/home/giacomo/.codex/version.json"
      },
      "remediation": null,
      "durationMs": 1052
    }
  }
}

What issue are you seeing?

Summary

A long-lived codex process grows its open file-descriptor count without
bound. I have now observed it reach ~650,000 open fds on two separate
occasions. On FreeBSD (and, I believe, macOS) each of these is a kqueue
EVFILT_VNODE registration — i.e. one open fd per watched filesystem path —
so the fd table is really a table of ~650k file watches that are never
released.

Idle baseline for the whole machine is ~2,700 open fds; a single codex
session drove the system-wide total to 668,507.

Evidence

While the leaked process was alive:

kern.openfiles: 668507          # system-wide; baseline ~2720
KNOTE UMA zone:  657555 in use   # vmstat -z; baseline a few thousand

The process itself was idle (0.0% CPU, state I+) — it had finished its
work and was just sitting there holding the watches.

Killing that single codex process dropped the system-wide count from
668,507 back to 2,720. So essentially all ~665k fds belonged to codex.

The kqueue thread's kernel stack (from an earlier incident, same machine)
showed the notify/kqueue backend as the owner:

codex  "notify-rs kqueue loop"  ... kern_proc_kqueues_out ...

which is what identifies these as notify-rs (or equivalent) vnode watchers.

Why this looks like a leak, not legitimate watching

The working directory codex was launched in contained 6 files and 4
directories
. The widest ancestor it could plausibly have walked up to held
88,305 files. Neither is remotely close to 650,000. So the fd count is not
explained by the size of the watched tree — watches appear to be
re-registered and/or never dropped over the lifetime of the session.

I was not able to dump the exact list of watched paths: with an fd table
that large, the OS call to enumerate it (procstat -f) itself blocks for
minutes (see below), so I can confirm the count and the type of the
descriptors but not the precise set of paths. If you can point me at a codex
debug/verbose mode that logs watch registrations, I can capture that on the
next occurrence.

Secondary impact on the host (the reason this is more than cosmetic)

On FreeBSD, an fd table of this size makes the process effectively
un-introspectable and can wedge unrelated processes:

  1. A crash of the leaked process turns into an hours-long, unkillable,

CPU-pinning core dump (the kernel serialises all 650k knotes into the
core file, twice). Filed separately as FreeBSD bug 296835.

  1. Even with no crash, any process that reads the leaked process's fd table

inherits a multi-minute uninterruptible syscall. In my case a terminal
emulator (Konsole), which polls the foreground process's working
directory to draw its tab title, spun at 99% CPU for 99 minutes on a
single such call and froze its window.

Neither of these is codex's fault to fix — they are FreeBSD-side
robustness issues — but they are downstream consequences of the fd count,
and they make an otherwise-invisible leak very painful in practice. Bounding
the watcher count would prevent both.

  • I have seen the ~670k figure in both incidents, which may indicate a

plateau/ceiling rather than strictly unbounded growth, but I could not
confirm that with the process still alive.

  • Happy to gather more detail (specific watched paths, growth rate over

time) if there is a codex-side logging switch for the file watcher.

What steps can reproduce the bug?

  1. On FreeBSD (ZFS), run codex in a project and use it across a reasonably

long session with file edits.

  1. Periodically observe sysctl kern.openfiles (or, per-process,

procstat -f <codex-pid> | wc -l while the count is still small).

  1. The count climbs and does not come back down; it does not track the size

of the working tree.

What is the expected behavior?

_No response_

Additional information

  • Cap or pool the number of concurrent vnode watches, and ensure watches are

released when the corresponding files/dirs are no longer of interest.

  • On the kqueue backend specifically, be aware that each watch is an open fd

— so an unbounded watch set is an unbounded fd set, which hits per-process
and system fd limits and the secondary effects above.

  • A configurable ceiling (with a warning when hit) would at least make the

failure mode visible rather than silent

View original on GitHub ↗

3 Comments

ianthetechie · 24 days ago

I have observed something similar as well in the past with long-lived codex processes on FreeBSD. I also noticed something vaguely similar on macOS with resource use, but I did not think to investigate if this was the cause or not (surely they have diverged some over the years?).

FWIW FreeBSD 15 introduced a native inotify syscall, so that might be easier for the team to adapt to.

ianthetechie · 22 days ago

It looks like there might be a mitigation upstream in the meantime: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=296835. Though it'll be a while till that makes it in a release I suspect.

ianthetechie · 16 days ago

Quick bump on this. I'm sure there are people in the community who would be open to helping with this, but this repo states contributions are not accepted.

This is a persistent ongoing issue with the Codex CLI, and the long-term fix is using the inotify API in my opinion. I just hit an issue today where it actually exhausted file handle limits and caused system instability.