hashicorp/nomad · warning
process did not exit after 15 seconds
Error message
process did not exit after 15 seconds
What it means
After sending the shutdown signal, Shutdown waits up to 15 seconds on the processExited channel. If the child has not exited by then, the executor logs a warning and appends this error to the shutdown multi-error, signaling the task ignored the kill signal and may need forced cleanup (e.g. cgroup kill).
Source
Thrown at drivers/shared/executor/executor.go:676
case <-e.processExited:
case <-time.After(grace):
proc.Kill()
}
} else {
proc.Kill()
}
// Issue sigkill to the process group (if possible)
if err = e.killProcessTree(proc); err != nil {
e.logger.Warn("failed to shutdown process group", "pid", proc.Pid, "error", err)
}
// Wait for process to exit
select {
case <-e.processExited:
case <-time.After(time.Second * 15):
e.logger.Warn("process did not exit after 15 seconds")
merr.Errors = append(merr.Errors, fmt.Errorf("process did not exit after 15 seconds"))
}
if err = merr.ErrorOrNil(); err != nil {
// Note that proclib in the TR shutdown may also dispatch a final platform
// cleanup technique (e.g. cgroup kill), but if we get to the point where
// that matters the Task was doing something naughty.
e.logger.Warn("failed to shutdown due to some error", "error", err.Error())
return err
}
return nil
}
// Signal sends the passed signal to the task
func (e *UniversalExecutor) Signal(s os.Signal) error {
if e.childCmd.Process == nil {
return fmt.Errorf("Task not yet run")
}View on GitHub (pinned to 482b49bf1a)
Solutions
- Configure a signal the app actually handles (e.g. kill_signal = "SIGTERM") so graceful shutdown works
- Fix the application to exit promptly on the shutdown signal
- Rely on the subsequent forceful cleanup (cgroup kill / Kill) that follows this timeout
- Increase application-level shutdown timeout handling rather than expecting the 15s wait to extend
Example fix
// before (task config) # no kill_signal -> defaults to SIGINT which the app ignores // after kill_signal = "SIGTERM"
Defensive patterns
Strategy: fallback
Validate before calling
// Prefer a signal the application handles
cfg := task.Config
if cfg.KillSignal == "" {
cfg.KillSignal = "SIGTERM" // only if app handles SIGTERM better than SIGINT
} Type guard
func handlesSignal(appCmd []string, sig string) bool {
// out of band: inspect app docs/config; here just confirm signal is valid
_, ok := signals.SignalLookup[sig]
return ok
} Try / catch
if err := exec.Shutdown(); err != nil {
if strings.Contains(err.Error(), "did not exit after 15 seconds") {
log.Warn("graceful shutdown timed out; escalating to force kill")
_ = exec.Kill() // fallback to hard kill / cgroup cleanup
return nil
}
return err
} Prevention
- Configure kill_signal to match the application's documented shutdown signal
- Make applications exit promptly on the shutdown signal
- Avoid trapping and ignoring SIGINT/SIGTERM in task processes
- Set realistic internal shutdown hooks well under 15 seconds
When it happens
Trigger: Calling Shutdown on a task that traps/ignores SIGINT (the default signal) and does not exit within 15 seconds; a hung or deadlocked child process; a child that spawned grandchildren keeping the process group alive.
Common situations: Applications that handle SIGINT interactively (shells, REPLs); zombie or stuck workers; tasks with long-running shutdown hooks exceeding the 15s window.
Related errors
- process failed to exit after 15 seconds
- executor failed to shutdown error: no process found
- rcp.connection_write_timeout must be greater than zero
- rcp.stream_close_timeout must be greater than zero
- rcp.stream_open_timeout must be greater than zero
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/4631af4adbeb3574.
Report an issue: GitHub.