# How to Ensure Process Group Termination for Freeing Metal-Wired Memory on Unix in Electron Apps

> Prevent GPU memory leaks in Electron apps on Unix. Learn to ensure process group termination and free Metal-wired memory by detaching child processes and killing the process group.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: how-to-guide
- Published: 2026-08-21

---

**To prevent GPU memory leaks in Electron apps that spawn child processes on macOS or Linux, spawn the child with `detached: true` to create a new process group, then terminate the entire group using `process.kill(-pid, 'SIGKILL')` when the application exits.**

When an Electron application embeds a Python FastAPI server or other native subprocesses on macOS, child processes that allocate Metal-wired GPU memory can become orphaned when the main process exits, causing permanent memory leaks until system reboot. According to the Modly source code, the solution involves creating a dedicated process group for the Python bridge and ensuring the entire group receives a SIGKILL signal during shutdown, guaranteeing that all GPU-bound subprocesses release their resources immediately.

## Understanding the Metal-Wired Memory Leak Risk

On macOS and other Unix platforms, child processes spawned by your Electron main process may allocate **Metal-wired memory** for GPU acceleration. If these processes survive after the Electron app exits—either because they ignored the initial termination signal or because they were spawned by the Python server itself—they retain their GPU memory allocations indefinitely. Since wired memory cannot be swapped out, this leaks system resources that only a manual `kill` command or system reboot can reclaim.

The root cause is that default process termination only targets the immediate child process. If that child has spawned its own children (grandchildren of the Electron main process), those orphan processes may continue running with no parent to clean them up.

## Creating a Dedicated Process Group

The solution implemented in [`electron/main/python-bridge.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/python-bridge.ts) uses Unix process groups to ensure that a single kill command reaches every descendant process.

### Spawn the Child in a New Process Group

When spawning the Python FastAPI server on non-Windows platforms, set the `detached` option to `true`. This forces the child to become the leader of a new process group, isolating it from the parent's process group:

```typescript
import { spawn } from 'child_process';

// In electron/main/python-bridge.ts (lines 63-69)
const child = spawn(pythonExecutable, ['-m', 'uvicorn', 'main:app'], {
  cwd: apiDirectory,
  env: { ...process.env, ...customEnv },
  detached: process.platform !== 'win32', // Creates new process group on Unix
});

```

On Unix systems, this `detached` flag makes the child process a **process group leader**. All subsequent processes spawned by this Python server—including extension runners—inherit this group ID (PGID).

### Store the Process Reference

Maintain a reference to the spawned process so you can target its group later. In [`electron/main/python-bridge.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/python-bridge.ts) (lines 15-17), the class stores the child process instance:

```typescript
private process: ChildProcess | null = null;

// Later, after spawning:
this.process = child;

```

The `pid` of this child process serves as the **process group ID (PGID)** because it is the group leader.

## Terminating the Entire Process Group

When the Electron application shuts down, you must kill every process in the group, not just the parent. In [`electron/main/python-bridge.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/python-bridge.ts) (lines 13-22), the negative PID syntax tells Node.js to send the signal to the entire process group rather than a single process.

### Unix Implementation

Use `process.kill(-pid, 'SIGKILL')` to force-terminate the entire group:

```typescript
function stopPythonBridge(): void {
  if (!this.process) return;
  
  const proc = this.process;
  this.process = null;

  if (proc.pid && process.platform !== 'win32') {
    try {
      // Negative PID targets the entire process group (lines 13-22)
      process.kill(-proc.pid, 'SIGKILL');
    } catch (error) {
      // If group already terminated, fall back to single process kill
      proc.kill('SIGKILL');
    }
  }
}

```

The negative sign before `proc.pid` is the critical Unix feature that converts the PID into a PGID target, ensuring SIGKILL reaches the Python server and all its Metal-allocating grandchildren simultaneously.

### Windows Fallback

Windows does not support Unix process groups or negative PID signaling. For Windows compatibility, use `taskkill` with the `/T` flag to terminate the process tree:

```typescript
import { execSync } from 'child_process';

// In electron/main/python-bridge.ts (lines 9-12)
if (process.platform === 'win32' && proc.pid) {
  execSync(`taskkill /PID ${proc.pid} /T /F`);
}

```

## Integrating with Electron Lifecycle

Process termination must hook into Electron's shutdown sequence to prevent the main process from exiting before children are killed.

### Before-Quit Event Hooks

In [`electron/main/ipc-handlers.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/ipc-handlers.ts) (lines 1434-1435), the application registers cleanup functions that run when the user quits the app:

```typescript
import { app } from 'electron';
import { terminateAllProcessRunners } from './process-runner';

// Ensure all JS workers and Python processes stop before quit
app.on('before-quit', () => {
  terminateAllProcessRunners(); // Kills Node-based ProcessRunners
  stopPythonBridge();             // Kills Python process group
});

```

The `terminateAllProcessRunners()` function, defined in [`electron/main/process-runner.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/process-runner.ts), handles any JavaScript-based worker threads or child processes, while `stopPythonBridge()` handles the Python process group using the negative PID technique described above.

## Complete Working Example

Here is the minimal implementation combining all components:

```typescript
import { spawn, ChildProcess } from 'child_process';
import { app } from 'electron';
import { execSync } from 'child_process';
import { terminateAllProcessRunners } from './process-runner';

class PythonBridge {
  private process: ChildProcess | null = null;

  start() {
    const child = spawn('python', ['-m', 'uvicorn', 'main:app'], {
      detached: process.platform !== 'win32', // Process group on Unix
    });
    
    this.process = child;
  }

  stop() {
    if (!this.process) return;
    const proc = this.process;
    this.process = null;

    if (process.platform === 'win32') {
      // Windows: Kill process tree
      execSync(`taskkill /PID ${proc.pid} /T /F`);
    } else if (proc.pid) {
      // Unix: Kill entire process group with SIGKILL
      try {
        process.kill(-proc.pid, 'SIGKILL');
      } catch {
        proc.kill('SIGKILL');
      }
    }
  }
}

const bridge = new PythonBridge();

// Lifecycle integration
app.on('before-quit', () => {
  terminateAllProcessRunners();
  bridge.stop();
});

```

## Summary

- **Process group creation**: Set `detached: true` when spawning child processes on Unix to create a new process group led by the child PID.
- **Group termination**: Use `process.kill(-pid, 'SIGKILL')` to send the kill signal to every process in the group, ensuring Metal-wired memory is released.
- **Platform differences**: Windows requires `taskkill /T` instead of negative PID signaling.
- **Lifecycle hooks**: Register cleanup in `app.on('before-quit')` to prevent orphaned processes when the Electron window closes.
- **Source files**: Implement this logic in [`electron/main/python-bridge.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/python-bridge.ts) and integrate it via [`electron/main/ipc-handlers.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/ipc-handlers.ts) using `terminateAllProcessRunners()`.

## Frequently Asked Questions

### Why does Metal-wired memory persist after the Electron app quits?

Metal-wired memory is allocated by child processes—such as Python extension runners—that create GPU resources on macOS. If these children are not terminated when the Electron main process exits, they become orphaned zombie processes that the operating system cannot clean up automatically. Because wired memory is locked and cannot be swapped, it remains allocated until the processes are killed or the system reboots.

### What is the significance of the negative PID in process.kill()?

In Unix systems, passing a negative value to `process.kill()` interprets the number as a **process group ID (PGID)** rather than an individual process ID. This sends the signal to every process in that group simultaneously. According to the Modly implementation in [`electron/main/python-bridge.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/python-bridge.ts), `process.kill(-proc.pid, 'SIGKILL')` ensures the Python server and all its descendants—including those that allocated Metal resources—receive the termination signal together.

### How does this approach handle Windows compatibility?

Windows does not support Unix-style process groups or negative PID arguments. The Modly codebase handles this platform difference by checking `process.platform` and falling back to `execSync(\`taskkill /PID ${pid} /T /F\`)` on Windows. The `/T` flag terminates the entire process tree, achieving equivalent cleanup behavior to the Unix process group kill.

### Should I use SIGTERM or SIGKILL for terminating GPU-bound processes?

Use **SIGKILL** for processes that may have allocated Metal-wired memory. SIGTERM allows the process to handle cleanup logic, which may fail or hang if the process is blocked on GPU operations. SIGKILL forces immediate termination without cleanup handlers, ensuring the GPU memory is released even if the process is unresponsive. The Modly source code explicitly uses `'SIGKILL'` in `process.kill(-proc.pid, 'SIGKILL')` to guarantee resource release.