Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Linux Admins By James Joyner IV · · 9 min read Last reviewed Jul 2026

Linux Error Guide: 'Terminated by signal 2' — Handle SIGINT and Ctrl-C Cleanly

Quick answer

Understand 'Terminated by signal 2' on Linux: why Ctrl-C sends SIGINT, why the exit code is 130, and how to trap the signal so scripts and services shut down cleanly.

  • #linux
  • #troubleshooting
  • #errors
  • #signals
Free toolkit

Stuck on this Linux Admins error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

A process ended because it received signal 2 — SIGINT, the interrupt signal. Most commonly this is a human pressing Ctrl-C in the terminal, but it can also come from a parent process, a supervisor, or a kill -2 sent by a script. The kernel reports the termination in messages like these:

$ ./long-running-job.sh
^C
Terminated by signal 2

$ echo $?
130

The exit code 130 is the convention for a process killed by SIGINT: 128 + signal_number where the signal number is 2. Seeing Terminated by signal 2 or an exit status of 130 almost always means the process was interrupted rather than crashing on its own.

Symptoms

  • A foreground command stops immediately after you press Ctrl-C.
  • echo $? returns 130 right after the process exits.
  • Shell scripts abort partway through a loop with Terminated by signal 2 in the output.
  • A CI job or wrapper script logs exited with code 130 and marks the step as failed.
  • dmesg, journalctl, or a supervisor log shows the child received SIGINT and shut down.
  • Cleanup that should run at exit (temp files, locks, child processes) is skipped because the trap was never installed.

Common Root Causes

  • Interactive Ctrl-C — the operator interrupted a foreground job; the terminal driver sends SIGINT to the entire foreground process group.
  • Parent forwarding the signal — a wrapper script or shell received SIGINT and the child in the same process group also got it.
  • Explicit kill -2 <pid> / kill -INT — a deploy or watchdog script sent SIGINT deliberately to request a graceful stop.
  • Supervisor stop policy — a process manager (systemd with KillSignal=SIGINT, tini, dumb-init, or a language runtime) sends SIGINT before SIGTERM.
  • No signal handler / trap — the program treats SIGINT as the default action (terminate) and exits without cleanup.
  • Nested subshells — a script backgrounds work but does not forward or trap SIGINT, so children die abruptly.

Diagnostic Workflow

Confirm the exit code and decode it into a signal number:

./your-command
echo "exit code: $?"          # 130 means 128 + 2 (SIGINT)
kill -l 2                     # prints: INT  (signal 2 = SIGINT)

List the signals a running process is currently ignoring, catching, or blocking (read the bitmask fields):

grep -E 'SigIgn|SigCgt|SigBlk' /proc/$(pgrep -f long-running-job)/status

See who or what may be sending the signal by watching the process tree and the terminal’s foreground group:

ps -o pid,ppid,pgid,stat,cmd -p "$(pgrep -f long-running-job | head -1)"

If a systemd unit is involved, check the configured kill signal and the last exit reason:

systemctl show your.service -p KillSignal -p KillMode
journalctl -u your.service --since '10 min ago' | grep -iE 'signal|SIGINT|stopped'

Reproduce interactively and observe the interrupt in real time:

sleep 300 &                  # background a dummy job
kill -INT %1                 # send SIGINT to it
wait; echo $?                # 130 confirms SIGINT termination

Example Root Cause Analysis

A backup script leaves stale lock files whenever an operator cancels it. The team assumed the script was crashing. Running it and pressing Ctrl-C showed:

$ ./nightly-backup.sh
Creating snapshot...
^C
$ echo $?
130
$ ls /var/run/backup.lock
/var/run/backup.lock          # lock left behind

The exit code 130 proved the script was interrupted by SIGINT, not failing internally. Inspecting the script revealed no trap for cleanup — the default SIGINT action terminated the shell instantly, before the rm -f "$LOCKFILE" line at the end could run. Adding a trap fixed both the stale lock and orphaned child processes:

#!/usr/bin/env bash
set -euo pipefail
LOCKFILE=/var/run/backup.lock

cleanup() {
  rm -f "$LOCKFILE"
  # forward the interrupt so callers still see exit 130
  trap - INT
  kill -INT "$$"
}
trap cleanup INT TERM EXIT

After the change, cancelling with Ctrl-C removed the lock, killed children, and still exited with 130 so CI and wrappers correctly saw an interrupted run rather than a false success.

Prevention Best Practices

  • Install a trap 'cleanup' INT TERM EXIT in any script that creates temp files, locks, or child processes.
  • Preserve the correct exit code: after cleanup, re-raise SIGINT (trap - INT; kill -INT "$$") so the exit status stays 130.
  • In long-running services, register real SIGINT handlers (Python signal.signal, Go signal.Notify, Node process.on('SIGINT')) to drain work before exiting.
  • Forward signals to children in wrapper scripts, or use exec so the child replaces the shell and receives signals directly.
  • For containers, use an init like tini or dumb-init (or docker run --init) so PID 1 reaps children and forwards SIGINT properly.
  • Treat exit code 130 in CI as “interrupted,” distinct from application failures, so retries and alerts behave sensibly.

Quick Command Reference

echo $?                                  # 130 = terminated by SIGINT
kill -l 2                                # map signal number 2 -> INT
kill -INT <pid>                          # send SIGINT to a process
trap 'cleanup' INT TERM EXIT             # run cleanup on interrupt/exit
grep Sig /proc/<pid>/status              # inspect caught/ignored signals
systemctl show <unit> -p KillSignal      # supervisor's stop signal
stty -a | grep intr                      # show which key sends SIGINT (^C)

Conclusion

Terminated by signal 2 and exit code 130 are not crashes — they are a clean report that the process received SIGINT, almost always from Ctrl-C or a deliberate kill -2. Decode the exit code (128 + signal), confirm the source with /proc/<pid>/status and your supervisor’s config, and add a trap or an in-process signal handler so interruptions run your cleanup and preserve the correct exit status. Handled well, SIGINT becomes a graceful-shutdown feature rather than a source of stale locks and orphaned processes.

Free download · 368-page PDF

Fixed it? Get 500 Linux Admins & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.