neondatabase/neon · error

Failed to send signal to {process_name} with pid {pid}: {e}

Error message

Failed to send signal to {process_name} with pid {pid}: {e}

What it means

stop_process resolves the pid from the pid file and sends SIGTERM (or SIGKILL for immediate mode) with nix::sys::signal::kill. ESRCH (process gone) is handled gracefully, but any other errno is fatal: in practice almost always EPERM, meaning the current user lacks permission to signal that pid. The pid file is deliberately left in place on this path.

Source

Thrown at control_plane/src/background_process.rs:213

    // send signal
    let sig = if immediate {
        print!("Stopping {process_name} with pid {pid} immediately..");
        Signal::SIGQUIT
    } else {
        print!("Stopping {process_name} with pid {pid} gracefully..");
        Signal::SIGTERM
    };
    io::stdout().flush().unwrap();
    match kill(pid, sig) {
        Ok(()) => (),
        Err(Errno::ESRCH) => {
            // Again, don't delete the pid file. The unlink can race with a new pid file being created.
            println!(
                "{process_name} with pid {pid} does not exist, but a pid file {pid_file:?} was found. Likely the pid got recycled. Lucky we didn't harm anyone."
            );
            return Ok(());
        }
        Err(e) => anyhow::bail!("Failed to send signal to {process_name} with pid {pid}: {e}"),
    }

    // Wait until process is gone
    wait_until_stopped(process_name, pid)?;
    Ok(())
}

pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
    for retries in 0..STOP_RETRIES {
        match process_has_stopped(pid) {
            Ok(true) => {
                println!("\n{process_name} stopped");
                return Ok(());
            }
            Ok(false) => {
                if retries == NOTICE_AFTER_RETRIES {
                    // The process is taking a long time to start up. Keep waiting, but
                    // print a message

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Identify the owner: `ps -o user= -p <pid>` and align by stopping with the same user that started it (`sudo neon_local env stop` or the equivalent).
  2. Kill the process directly as the right user: `sudo kill <pid>` (SIGTERM first, then SIGKILL), then re-run stop or remove the stale pid file once dead.
  3. Audit your workflow so start and stop always run under the same account (never mix sudo and non-sudo for one env).
  4. If this recurs in CI, stop processes in the same container/shell context that started them.

Example fix

// before
stop_process(immediate, process_name, &pid_file)?; // EPERM: Failed to send signal

// after
match stop_process(immediate, process_name, &pid_file) {
    Err(e) if e.to_string().contains("Failed to send signal") => {
        let pid = pid_file::read(&pid_file)?.unwrap_pid();
        std::process::Command::new("sudo")
            .args(["kill", &pid.to_string()])
            .status()?;
    }
    other => other?,
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify you can signal the pid before attempting a graceful stop
let pid = match pid_file::read(&pid_file)? { pid_file::PidFileRead::LockedByOtherProcess(p) => p, _ => return Ok(()) };
match kill(pid, None) {
    Ok(_) => (),
    Err(nix::errno::Errno::EPERM) => /* need elevated privileges */,
    Err(nix::errno::Errno::ESRCH) => return Ok(()),
    Err(e) => return Err(e.into()),
}

Try / catch

match stop_process(immediate, process_name, &pid_file) {
    Err(e) if e.to_string().contains("Failed to send signal") => {
        // cross-privilege stop: re-signal via sudo, then retry
        std::process::Command::new("sudo").args(["kill", "--", &pid.to_string()]).status()?;
        stop_process(immediate, process_name, &pid_file)
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling stop_process when the target process (pageserver/safekeeper/compute) was started by a different user (root vs your user), or in a container where the process runs under a different UID. Rarely EINVAL from an invalid signal number after code changes.

Common situations: Mixing sudo-launched and user-launched processes in one neon_local env; running `neon_local start` as root once and later stopping as a normal user (or vice versa); SELinux/AppArmor denying signals; leftover processes owned by another user after a CI job ran as a different account.

Related errors


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