skip to content

What does the Go runtime do when a process receives SIGQUIT and nothing has trapped it?

level: middleimportance: should knowfreq 44%

answer

  1. the poor man's thread dump
  2. a signal, not a flag
  3. everything prints, then nothing runs
  4. Ctrl-backslash at a terminal
  5. unless the program took the signal

basics

~20 s

An untrapped SIGQUIT is fatal to a Go process: the runtime prints a header naming the signal, then a stack trace for every goroutine, and the process then dies. It is a one-shot snapshot, not something the process survives.

solid answer

~40 s

SIGQUIT - `kill -QUIT <pid>`, or Ctrl-\ at a terminal - is the standard way to get a full goroutine dump out of a Go process that is alive but stuck. The runtime treats it as a fatal condition, so it prints a line naming the signal and then a stack for **every** goroutine, not just the current one, even at the default GOTRACEBACK=single. Two things take that away from you: GOTRACEBACK=none suppresses the stacks, and any code that calls `signal.Notify` for SIGQUIT takes the signal over, which disables the runtime's dump entirely. The output goes to standard error, so it is only useful if something is capturing that. And the process terminates afterwards, which makes this a decision rather than a free diagnostic.

code

go · 5 lines
go
c := make(chan os.Signal, 1)
// Adding SIGQUIT here disables the runtime's stack dump for the whole process.
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
<-c
srv.Shutdown(ctx)

go deeper

for a junior

Know that sending SIGQUIT to a Go process makes it print every goroutine's stack to standard error and then exit, and that Ctrl-backslash does the same thing at a terminal.

for a middle

Explain why every goroutine prints even at the default GOTRACEBACK level, and name the two ways the dump disappears: the none level, and a program that registered SIGQUIT with signal.Notify.

for a senior

Weigh the cost before you send it, because the process dies. Say when you would take that - a stuck instance with healthy peers, or an agent a supervisor restarts - and how you make sure standard error is being captured before you pull the trigger.

for a principal

Set the platform rule: whether services may trap SIGQUIT at all, where fatal output is collected so a dump survives the restart, and what an operator is allowed to do to a production process at 3am without raising a change request.

## The mechanism The Go runtime installs handlers for the signals it cares about. SIGQUIT is in the class it treats as a **fatal, throw-style failure**: not a request to shut down, but an instruction to report what the program is doing and stop. So when SIGQUIT is delivered and the program has not taken the signal over itself, the runtime prints a header line naming the signal, then walks every goroutine and prints its stack, then terminates the process. The important detail is the word *every*. The default GOTRACEBACK level, `single`, restricts an ordinary panic report to the failing goroutine - but a failure the runtime considers internal, which a fatal signal is, prints all goroutines regardless. That is why `kill -QUIT` works as a full dump on an unmodified binary with nothing configured: you do not have to have set GOTRACEBACK=all in advance, which is exactly what makes it the tool you reach for on a process that is already running and already sick. ``` kill -QUIT 4213 # or press Ctrl-\ in the terminal running it ``` ## What it is for It answers one question: *what is this process doing right now?* A Go service that is alive, holding its port, and serving nothing gives you almost no other evidence from outside. A full goroutine dump names every goroutine, its state and its stack, and that is usually enough to see whether they are all parked on the same thing, all in a syscall, or all working hard on nothing. ## The three ways it lets you down **1. The process dies.** Unlike a thread dump in a runtime that offers one as a supported live operation, this is terminal. On an instance behind a load balancer with healthy peers, or an agent that a supervisor will restart in a second, that is an acceptable price. On a singleton with state in memory, it is an outage you chose, and you should say so out loud before you type it. **2. GOTRACEBACK=none.** The signal still kills the process, but the stacks are suppressed, so you traded the process for nothing. Any service that runs with the quietest level has effectively opted out of this tool. **3. The program trapped the signal.** `signal.Notify` (and `signal.Ignore`) tell the runtime that the program wants the signal itself, and once that is true the runtime's default behaviour - dump and die - does not happen. This is a common accident: a shutdown handler is written to catch "the termination signals", someone adds SIGQUIT to the list next to SIGINT and SIGTERM, and from that release onwards operators quietly lose the ability to snapshot the process. Nothing warns you; the signal simply lands in a channel and triggers a graceful shutdown instead. ## Where the output goes Standard error, written synchronously as the process dies. That is fine at a terminal and fine under a supervisor that captures stderr into a journal or log file. It is worthless if the process is started with standard error redirected to `/dev/null`, or if the supervisor keeps only a small ring buffer, because a full dump of a process with thousands of goroutines will overflow it and you will keep the tail rather than the part you wanted. A program that expects to be debugged this way can register an additional destination for fatal output from inside itself, so the report also lands in a file on local disk that a later upload can carry off the machine. ## What GOTRACEBACK still controls Breadth is already maximal, but detail is not. `system` adds run-time function frames and the goroutines the runtime created for itself, which is what you want if you suspect the failure is below your code rather than in it. `crash` goes further and aborts the process so the operating system can write a core. And `none`, as above, throws the whole thing away. ## What to say in an interview Say what it produces (every goroutine's stack, at the default level, on standard error), say what it costs (the process), and name the two ways it silently stops working: the quietest GOTRACEBACK level, and a program that registered SIGQUIT with `signal.Notify`. The last one is the answer that shows you have been burned by it.

  • How does a program accidentally disable the SIGQUIT dump?
    By registering SIGQUIT with `signal.Notify` - usually inside a generic shutdown handler that lists SIGINT, SIGTERM and SIGQUIT together. Once the program is receiving the signal, the runtime's dump-and-die behaviour no longer runs, and operators lose the tool with no warning and no log line.
  • Where does the dump go, and what if the supervisor discards it?
    To standard error, written synchronously as the process dies. If the process was started with standard error redirected away, or the supervisor keeps only a small buffer, the report is lost or truncated to its tail. A program that expects this can register an extra destination for fatal output so the report also lands on local disk.
  • Does GOTRACEBACK still matter for a SIGQUIT dump?
    For breadth, no - every goroutine already prints at the default level, because the runtime treats a fatal signal as an internal failure. For detail, yes: system adds run-time frames and the runtime's own goroutines, and none suppresses the stacks entirely, which means you killed the process and got nothing.

It is a photograph taken by demolishing the building: complete, immediate, and the subject does not survive it.

saying these in an interview costs you the question

  • Thinks SIGQUIT dumps the stacks and lets the process keep running
  • Says GOTRACEBACK=all must be set first to see every goroutine
  • Traps SIGQUIT alongside SIGTERM for graceful shutdown without noticing the loss
  • Confuses SIGQUIT with SIGTERM, which kills a Go process with no dump
  • Expects the dump on standard output rather than standard error