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 messageView on GitHub (pinned to 8f60b04da4)
Solutions
- 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).
- 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.
- Audit your workflow so start and stop always run under the same account (never mix sudo and non-sudo for one env).
- 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
- Start and stop all processes in an env as the same user; never mix sudo and non-sudo.
- Check `ps -o user= -p <pid>` when a stop mysteriously fails with EPERM.
- In CI, run setup and teardown in the same container/user context.
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
- Failed to send signal to process with pid {pid}: {err}
- {} with pid {} did not stop in {:?} seconds
- `datadir` must be a directory when calling this function: {d
- {} did not start+pass status checks within {:?} seconds
- process failed to start: {e}
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/ce0d53cbb140475c.
Report an issue: GitHub.