Skip to content

System-wide PTY exhaustion due to file descriptor leak #15945

@paulirish

Description

@paulirish

What happened?

gemini-cli (v0.22.5) appears to be leaking Pseudo-Terminal (PTY) master handles (/dev/ptmx). Over time, this exhausts the macOS system limit (kern.tty.ptmx_max), which is typically 511. When I started debugging the problem i had 1058.

I think when my count crossed 1023 PTYs, I got the following problems in creating new tabs/sessions:

  • iTerm2 presented a dialog when trying to open a new session: "A session ended very soon after starting. Check that the command in profile "Default" is correct."
  • Terminal.app displayed: [forkpty: Device not configured] and [Could not create a new process and open a pseudo-tty.]

Investigation via lsof revealed that multiple node processes running gemini were holding hundreds of open handles to /dev/ptmx. One specific session had leaked 155 PTY handles and was holding a total of 495 file descriptors. Some of these sessions had been running for over two weeks (since Dec 22). (Though I had run /clear a few times, based on what i'm seeing in the json trajectories)

Image

What did you expect to happen?

I expected gemini-cli and its associated processes (including MCP servers and node-pty instances) to correctly close file descriptors and PTY handles when they are no longer in use, and to terminate cleanly instead of remaining alive as idle processes that accumulate leaked resources. ;)

Client information

Client Information
> /about
│ CLI Version                                   0.22.5                                                                               │
│ Git Commit                                    8daf2d34b                                                                            │
│ Model                                         gemini-3-flash-preview                                                               │
│ Sandbox                                       no sandbox                                                                           │
│ OS                                            darwin                                                                               │
│ Auth Method                                   gemini-api-key
  • Platform: macOS 15.7.3 (arm64)
  • Binary Path: ~/.homebrew/bin/gemini - installed via npm install -g
  • Node version: v24.11.1
  • Dependency: @lydell/node-pty-darwin-arm64 (version: 1.1.0)

Login information

Logged in via GEMINI_API_KEY environment variable.

Anything else we need to know?

The leak may be correlated with the usage of MCP servers. In one instance, a gemini process (PID 46302) was the parent of an MCP server process, yet the parent gemini process was the one accumulating the /dev/ptmx handles. (I only have 1 mcp server enabled currently and it rarely is used so... not sure about this)

Observations in shellExecutionService.ts

I noticed some logic in packages/core/src/services/shellExecutionService.ts that might be worth investigating:

  1. In executeWithPty (around line 467), ptyInfo.module.spawn(...) is called.
  2. If spawn fails (e.g., throwing "posix_spawnp failed"), the error is caught by the try...catch block (around line 733).
  3. The catch block issues a warning: [GEMINI_CLI_WARNING] PTY execution failed, falling back to child_process.
  4. It's possible that if node-pty opens the PTY master (/dev/ptmx) before the posix_spawn syscall actually fails, the resulting file descriptor might not be getting closed during the fallback to child_process.

Technical Evidence (lsof and Stack Traces)

PTY Handle Count per Process:

PID 46302: PTMX=155, FDs=495 | /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
PID 98406: PTMX=74,  FDs=255 | /usr/local/bin/node ~/.homebrew/bin/gemini
PID 94695: PTMX=51,  FDs=183 | /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
... [multiple other sessions with 10-30 PTMX handles each] ...

LSOF Detailed view for PID 46302:

node    46302 [USER]  txt       REG               1,18     85160           188016135 ~/homebrew/lib/node_modules/@google/gemini-cli/node_modules/@lydell/node-pty-darwin-arm64/pty.node
node    46302 [USER]   17u      CHR             15,155       0t0                 605 /dev/ptmx
node    46302 [USER]   20u      CHR             15,151       0t0                 605 /dev/ptmx
node    46302 [USER]   23u      CHR             15,152       0t0                 605 /dev/ptmx
... [truncated 150+ similar entries] ...

Stack Trace (Main Thread) of Leaking Process:
The process appears idle in the event loop while holding the leaked FDs:

    883 Thread_28021267   DispatchQueue_1: com.apple.main-thread  (serial)
    + 883 start  (in dyld) + 6076  [0x18a23ab98]
    +   883 node::Start(int, char**)  (in node) + 604  [0x1048298a8]
    +     883 node::NodeMainInstance::Run()  (in node) + 276  [0x1048c3744]
    +       883 node::SpinEventLoopInternal(node::Environment*)  (in node) + 256  [0x104771b7c]
    +         883 uv_run  (in node) + 408  [0x1056c5d1c]
    +           883 uv__io_poll  (in node) + 784  [0x1056d9874]
    +             883 kevent  (in libsystem_kernel.dylib) + 8  [0x18a59fd04]

System Limits:
sysctl kern.tty.ptmx_max = 511

Diagnostic Script

I generated this to help investigate PTY usage

pty_report.sh

#!/bin/bash

# PTY Usage Detailed Report
echo "PTY Usage Report"
echo "================="

# Get system PTY limits
PTY_MAX=$(sysctl -n kern.tty.ptmx_max 2>/dev/null || echo "Unknown")
PTY_TOTAL=$(lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1' | wc -l | xargs)

echo "System Max PTYs (kern.tty.ptmx_max): $PTY_MAX"
echo "Current Total Open PTY Handles:      $PTY_TOTAL"
echo ""

printf "%-8s %-6s %-25s %-35s %s\n" "PID" "PTYs" "Binary" "CWD" "Full Command"
echo "------------------------------------------------------------------------------------------------------------------------"

# Get PIDs and their ptmx counts from lsof
lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1 {print $2}' | sort | uniq -c | sort -nr | while read count pid; do
    if [ -z "$pid" ]; then continue; fi
    
    # Get command name
    cmd=$(ps -p $pid -o comm= 2>/dev/null)
    
    # Get full command line
    args=$(ps -p $pid -o args= 2>/dev/null)
    
    # Get CWD
    cwd=$(lsof -a -p $pid -d cwd -Fn 2>/dev/null | sed -n 's/^n//p')
    
    # Format and print the row
    printf "%-8s %-6s %-25.25s %-35.35s %.100s\n" "$pid" "$count" "$cmd" "$cwd" "$args"
done

My results:

PTY Usage Report
=================
PID      PTYs   Binary                    CWD                  Full Command
------------------------------------------------------------------------------------------------------------------------
1572     351    /Applications/iTerm.app/C /                   /Applications/iTerm.app/Contents/MacOS/iTerm2
46302    155    /usr/local/bin/node       ~/src/numderscore   /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
98406    74     /usr/local/bin/node       ~/src/chrome-devtoo /usr/local/bin/node ~/.homebrew/bin/gemini
94695    51     /usr/local/bin/node       ~/src/nemo          /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
34569    48     /Applications/Visual Stud /                   /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper
1724     39     ~/Library/ /                                  ~/Library/Application Support/iTerm2/iTermServer-3.6.4 ~/Library/Appli
30521    34     /usr/local/bin/node       ~/src/proj2         /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
38568    30     /usr/local/bin/node       ~/src/proj3         /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
22155    30     /usr/local/bin/node       ~/src/proj4         /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
88722    23     /usr/local/bin/node       ~/src/proj5         /usr/local/bin/node ~/src/gemini-cli/bundle/gemini.js
86950    19     /usr/local/bin/node       ~/devtools/         /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
17628    8      /usr/local/bin/node       ~/src/proj5         /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview

Metadata

Metadata

Assignees

Labels

area/coreIssues related to User Interface, OS Support, Core Functionalityhelp wantedWe will accept PRs from all issues marked as "help wanted". Thanks for your support!priority/p1Important and should be addressed in the near term.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions