What does a Go test binary print when the go test -timeout deadline expires?
answer
- one deadline for the whole binary
- it panics rather than failing quietly
- every goroutine, not just the stuck one
- repeated stacks mean one bug seen many times
- it only fires if something is still running
basics
~20 sIt panics with a message like "panic: test timed out after 10m0s", names the tests still running and how long they have run, and then prints the stack of every goroutine in the process. The package is reported as failed.
solid answer
~50 sThe `testing` package arms a timer for the whole test binary — ten minutes by default, `-timeout d` to change it, `-timeout 0` to disable. When it fires, the binary panics with `panic: test timed out after <d>`, lists the tests that were still running with their elapsed times, and dumps the stack of **every** goroutine in the process, not just the one that hung. That dump is the diagnostic: a goroutine parked on `chan send` or `chan receive` shows its state and its exact call site, so you can read straight off the output which send nobody is serving. Two limits are worth stating: the deadline is per test binary (that is, per package), not per test, so a slow suite can trip it with nothing wrong; and it only fires when something is still running at the deadline. A goroutine that leaks without blocking the test never triggers it, because the binary finishes and exits normally.
code
text · 14 linespanic: test timed out after 30s
running tests:
TestFanoutBroadcast (30s)
goroutine 1 [chan receive]:
testing.(*T).Run(...)
goroutine 47 [chan send]:
example/fanout.(*hub).broadcast(...)
/src/fanout/hub.go:88 +0x9c
goroutine 48 [chan send]:
example/fanout.(*hub).broadcast(...)
/src/fanout/hub.go:88 +0x9cgo deeper
Know that the deadline exists, that ten minutes is the default, and that when it fires the binary panics and prints every goroutine's stack rather than just failing the test quietly.
Explain that the deadline covers the whole test binary, that the output names the tests still running, and how a goroutine's parked state and call site in the dump point at the channel operation nobody is serving.
Demonstrate reading the dump: repeated identical stacks as one bug, long wait times as leftovers from an earlier test, and the honest limit that this catches hangs while silent leaks still need their own check.
Decide the policy: what deadline the pipeline sets relative to its job limit, whether lowering it is worth the flakiness it buys, and how timeouts are triaged so nobody's first move is to raise the number.
## What the flag actually arms `go test -timeout d` sets a deadline for the **test binary**, which means one deadline for the whole package's run, not per test function. The default is ten minutes. `-timeout 0` disables the deadline entirely, which is occasionally right for a long soak test and almost always wrong in a pipeline, because a hang then consumes the job's own limit and produces no diagnostic at all. ## What it prints When the deadline expires, the testing package raises the traceback level so that no environment setting can suppress the output, then panics. The result looks like this: ``` panic: test timed out after 30s running tests: TestFanoutBroadcast (30s) goroutine 1 [chan receive]: ... goroutine 47 [chan send]: example/fanout.(*hub).broadcast(...) /src/fanout/hub.go:88 +0x9c ``` Three pieces of information are in there, and each answers a different question. 1. **The panic line** tells you it was the deadline, not a crash. A test that fails this way did not assert anything; it simply never finished. 2. **The list of running tests** with elapsed times tells you which test was stuck. If several are listed, the suite was running them in parallel. 3. **The stack of every goroutine in the process** tells you *why*. This is the part that matters for a leak: each goroutine header carries its state — `chan send`, `chan receive`, `select`, `semacquire`, `IO wait`, `sync.WaitGroup.Wait` — and, once it has been parked long enough, roughly how long it has been there. The frames underneath name the exact line it is parked on. ## Reading it for a leak For a fan-out service that keeps one subscription goroutine per connected client, the useful pattern in that dump is repetition. Twenty goroutines sharing one stack, all in `chan send` inside the broadcast loop, is not twenty bugs — it is one bug seen twenty times: the receive side went away and nobody closed the channel or selected on a done signal. The line number under the repeated frame is the fix site. A second pattern worth recognising is a single goroutine parked with a very long wait time while the test that started it finished long ago. That is a leak from an **earlier** test showing up in a dump triggered by a **later** one. ## What it does not catch This is the crucial limitation, and it is the reason an explicit leak check exists alongside it. The deadline fires only if something is still running when it expires. A leaked goroutine that does not block the test does not keep the binary alive: the last test finishes, the process exits, every parked goroutine is discarded, and the package is reported as passing. So: - **`-timeout` catches hangs.** A goroutine the test is waiting on, a deadlock, a channel nobody serves on the critical path. - **It does not catch silent leaks.** A subscription goroutine nobody is waiting for leaks in complete silence and takes the service down in production instead. The two checks are complements, not alternatives. ## Practical use - Set a deadline well below your pipeline's job limit so you get the Go dump instead of a killed job with no output. - When a timeout appears, read the goroutine list before the stacks: which tests were running tells you where to start, and how many were running tells you whether parallelism is involved. - Do not lower the timeout to "catch problems earlier". A deadline tight enough to fire on a slow machine converts a capacity problem into a flaky failure, and the dump it produces looks identical to a real hang. - Do not reach for `-timeout 0` to make a stubborn timeout go away. It removes the only diagnostic you had.
- Is the -timeout deadline per test or per test binary?Per test binary, so effectively per package: one deadline covers the whole run of that package's tests. A suite of a hundred fast tests can therefore trip a ten-minute default purely by being long, with nothing hung at all — the output will list whichever test happened to be running when the timer fired, which is misleading if you read it as "the slow one". Per-test bounds have to come from the test's own logic.
- Will -timeout catch a goroutine that leaks without blocking the test?No. The deadline only fires if the binary is still running when it expires. A leaked subscription goroutine that nobody is waiting on does not hold the test up: the last test returns, the process exits, and every parked goroutine is discarded silently with a green result. That is precisely the gap an explicit leak check — comparing goroutine counts or profiles around the test — exists to close.
- Why does the dump include goroutines that have nothing to do with the timed-out test?Because a hang is usually caused by something the stuck goroutine is waiting *for*, which lives in a different goroutine — the sender that never sends, the worker holding the lock, the loop that returned early. Printing only the blocked goroutine would show you the symptom and hide the cause. It also means leftovers from earlier tests appear, which is often how a leak from one test is first noticed in another test's failure.
saying these in an interview costs you the question
- Thinks the timeout applies to each test function separately
- Believes a green suite proves nothing hung or leaked
- Uses -timeout 0 to silence a recurring timeout
- Reads only the first goroutine in the dump
- Treats repeated identical stacks as many separate bugs