Skip to content

Windows: Bun.spawn({ detached: true }) child is killed when the parent exits (does not outlive the parent) #31603

Open

Description

@BlackHole1

What version of Bun is running?

1.4.0-canary.1+7069b3092 (also reproduces on 1.3.14+0d9b296af)

What platform is your computer?

Microsoft Windows NT 10.0.26200.0 x64

What steps can reproduce the bug?

Complete, runnable reproduction: https://github.com/BlackHole1/bun-detached-windows-bug
(pwsh run.ps1 runs it and reports whether each spawned helper survived the parent's exit; includes a passing cmd /c start workaround variant and a multi-run stability check.)

The bug in one file — on Windows, a child spawned with Bun.spawn({ detached: true }) + unref() is killed the instant the parent Bun process exits, instead of outliving it. The same happens with node:child_process.spawn(..., { detached: true }) (Bun implements it on the
same primitive). The helper below waits for this (parent) process to exit, then writes marker.txt — a correctly-detached child should outlive the parent and create the file:

const parentPid = process.pid;

Bun.spawn({
  cmd: [
    "powershell", "-NoProfile", "-Command",
    `Wait-Process -Id ${parentPid} -ErrorAction SilentlyContinue; Set-Content -LiteralPath marker.txt -Value ran`,
  ],
  detached: true,
  stdin: "ignore",
  stdout: "ignore",
  stderr: "ignore",
  windowsHide: true,
}).unref();

console.log(`parent pid=${parentPid} spawned detached helper; exiting now`);
Remove-Item marker.txt -ErrorAction SilentlyContinue
bun repro.ts
Start-Sleep -Seconds 2
Test-Path marker.txt   # False (bug) — expected True

What is the expected behavior?

marker.txt is created — the detached helper outlives the parent and runs after it exits. This matches the documentation, which states detached "allow[s] the child to outlive the parent".

What do you see instead?

marker.txt is never created. The helper is terminated the moment the parent exits, so it never runs its post-exit work. It is fully deterministic — 0/3 over repeated runs on each build, on both stable (1.3.14) and canary (1.4.0-canary.1).

Additional information

The child appears to be a member of a kill-on-close job object that Bun uses to manage child processes. On Windows, detached maps to UV_PROCESS_DETACHED (which detaches the console / process group) but does not add CREATE_BREAKAWAY_FROM_JOB, so the child stays in the
job and is killed when the parent (the job handle owner) exits. This makes it impossible to launch a post-exit helper (self-update / self-uninstall / any cleanup that must run after the launching process is gone).

Same failure mode as nodejs/node#51018 on Windows.

Workaround (verified, survives 3/3 on both builds): launch through a cmd /c start "" /b trampoline, which creates the helper as a job-breakaway process. Await the bootstrap cmd so start has launched the helper before exiting:

const boot = Bun.spawn({
  cmd: [
    "cmd", "/c", "start", "", "/b",
    "powershell", "-NoProfile", "-Command",
    `Wait-Process -Id ${process.pid} -ErrorAction SilentlyContinue; Set-Content -LiteralPath marker.txt -Value ran`,
  ],
  stdin: "ignore",
  stdout: "ignore",
  stderr: "ignore",
  windowsHide: true,
});
await boot.exited;

Activity

  1. tvvignesh commented on May 30, 2026

    @tvvignesh

    I see this behavior as well in windows - works well in mac and linux. Had to use node:child_process cause of this to workaround

  2. BlackHole1 commented on Jun 1, 2026

    @BlackHole1
    Author

    @tvvignesh I remember node:child_process doesn't seem to work either, because in my impression, in Bun, node:child_process.spawn is actually a wrapper around Bun.spawn.

  3. BlackHole1 commented on Jun 1, 2026

    @BlackHole1
    Author

    @robobun Please try to reproduce and fix

  4. robobun commented on Sep 11, 2026

    @robobun
    Collaborator

    I looked at this while I worked on #42272. The job object is not the cause here. The helper is not killed: powershell.exe gives up at startup, because detached: true gives it no console.

    Measured on Windows Server 2019 x64 (Windows PowerShell 5.1), current main:

    import { openSync } from "node:fs";
    const log = openSync("ps.log", "a");
    const proc = Bun.spawn({
      cmd: ["powershell", "-NoProfile", "-Command", "Set-Content -LiteralPath marker.txt -Value ran; Write-Output hello; exit 5"],
      stdio: ["ignore", log, log],
      windowsHide: true,
      detached: true, // remove this line and it works
    });
    console.log(await proc.exited);
    • Without detached: exit code 5, marker.txt exists, ps.log has hello.
    • With detached: true: exit code 0 after about 40 ms, no marker.txt, ps.log is empty. The parent is still alive at that point, so nothing killed the child.

    On Windows, detached maps to libuv's UV_PROCESS_DETACHED, which passes DETACHED_PROCESS to CreateProcessW. The child has no console at all, and Windows PowerShell does not run a command without one. libuv does skip its kill-on-close job object for a detached child, so a program that can run without a console (a bun or node script, for example) does outlive the parent. The cmd /c start "" /b workaround works because cmd.exe gets a hidden console (windowsHide with no inherited stdio), PowerShell attaches to that console, and libuv's job does not extend to grandchildren.

    #42272 adds an opt-in windowsKeepAlive spawn option for this case: the child gets a new console that has no window and is not put in the job object. Your repro (helper waits for the parent to exit, then writes the marker) writes the marker with windowsKeepAlive: true in place of detached: true. It does not change what detached: true does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions