neondatabase/neon · error

Failed to send signal to process with pid {pid}: {err}

Error message

Failed to send signal to process with pid {pid}: {err}

What it means

process_has_stopped probes liveness with signal 0 via kill(pid, None): Ok means still alive, ESRCH means gone, and any other errno raises this error. In practice it is EPERM — the polling user lacks permission to signal the pid — which makes the stop loop abort instead of continuing to wait. It is the read-only twin of the stop_process signal error.

Source

Thrown at control_plane/src/background_process.rs:395

{
    match status_check().await {
        Ok(true) => match pid_file::read(pid_file_to_check)? {
            PidFileRead::NotExist => Ok(false),
            PidFileRead::LockedByOtherProcess(pid_in_file) => Ok(pid_in_file == pid),
            PidFileRead::NotHeldByAnyProcess(_) => Ok(false),
        },
        Ok(false) => Ok(false),
        Err(e) => anyhow::bail!("process failed to start: {e}"),
    }
}

pub(crate) fn process_has_stopped(pid: Pid) -> anyhow::Result<bool> {
    match kill(pid, None) {
        // Process exists, keep waiting
        Ok(_) => Ok(false),
        // Process not found, we're done
        Err(Errno::ESRCH) => Ok(true),
        Err(err) => anyhow::bail!("Failed to send signal to process with pid {pid}: {err}"),
    }
}

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Check ownership: `ps -o user= -p <pid>`; stop the process as the user that started it (e.g. prefix the stop command with sudo).
  2. Kill the leftover process directly as the correct user (`sudo kill <pid>`), then let stop_process see ESRCH and finish cleanly.
  3. Standardize one account for starting and stopping all processes in a neon_local env.
  4. If the pid is long gone but the error persists, verify with `kill -0 <pid>` from the same shell to reproduce the permission denial.

Example fix

// before
wait_until_stopped(process_name, pid)?; // EPERM on signal-0 probe

// after
// probe with the same privileges used to start the process
let alive = std::process::Command::new("sudo")
    .args(["kill", "-0", &pid.to_string()])
    .status()?.success();
if !alive { /* stopped */ }
Defensive patterns

Strategy: try-catch

Validate before calling

// before waiting, confirm signal permission with a signal-0 probe
match kill(pid, None) {
    Err(nix::errno::Errno::EPERM) => anyhow::bail!("no permission to probe pid {pid}; stop as the owning user"),
    _ => {},
}

Try / catch

match wait_until_stopped(process_name, pid) {
    Err(e) if e.to_string().contains("Failed to send signal to process") => {
        // permission problem, not a hang: switch privileges and re-probe
        std::process::Command::new("sudo").args(["kill", "-0", &pid.to_string()]).status()?;
        Ok(())
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling stop_process/wait_until_stopped when the target pid belongs to a different UID than the caller (process started under root, stopped under a normal user or vice versa). Also possible on systems with LSM policies denying signal-0 probes.

Common situations: Mixed-privilege workflows: one `neon_local` invocation under sudo, a later stop without it; CI containers where the compute process runs as another user; reusing an env directory across different accounts.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/72da576ee2ce41d8. Report an issue: GitHub.