Panic Test Timed Out After and How to Debug a Hung Test
Fix panic: test timed out after in Go. Read the goroutine dump, find the blocked test and what it waits on, and learn the usual causes and the timeout flag.

The error is this:
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
panic: test timed out after 10m0s
running tests:
TestSomething (10m0s)It means go test ran out of patience. Either one test is stuck, or the whole package has taken longer than the limit, which is ten minutes by default. The runtime kills the process and prints the stack of every goroutine, and that dump is long and looks hostile. It is also the most useful diagnostic Go gives you for a hang, if you know where to look.
This article reproduces the failure on Go 1.25.6, shows how to read the dump in four steps, lists the usual causes, and sets the flag so you find out in seconds, not minutes.
A test that hangs
package main
func main() {}package main
import (
"sync"
"testing"
)
func TestHangs(t *testing.T) {
var mu sync.Mutex
mu.Lock()
done := make(chan struct{})
go func() {
mu.Lock() // never gets the lock
close(done)
}()
<-done // the test waits for a goroutine that is stuck
}TestHangs takes a lock in the test goroutine and never releases it. It then starts a goroutine that tries to take the same lock before closing a channel, and waits on that channel. The goroutine can never get the lock, so it never closes the channel, and the test waits forever. It is a small deadlock, and it is the shape of many real ones: two parties each waiting for the other.
go test -count=1 -timeout 1s . 2>&1 | sed -E 's/0x[0-9a-f]+/0xADDR/g; s/[0-9]+\.[0-9]+s/<t>/g; s#/[^ ]*/(hang_test\.go)#\1#; s/goroutine [0-9]+/goroutine N/; s#/opt/homebrew/Cellar/go/[0-9.]+/libexec#$GOROOT#; s/in [0-9]+ minutes/in N minutes/; s/\+0xADDR//; s/\t[0-9.]+s$//' | grep -vE '^\s*$' | head -34panic: test timed out after 1s
running tests:
TestHangs (1s)
goroutine N [running]:
testing.(*M).startAlarm.func1()
$GOROOT/src/testing/testing.go:2682
created by time.goFunc
$GOROOT/src/time/sleep.go:215
goroutine N [chan receive]:
testing.(*T).Run(0xADDR, {0xADDR?, 0xADDR?}, 0xADDR)
$GOROOT/src/testing/testing.go:2005
testing.runTests.func1(0xADDR)
$GOROOT/src/testing/testing.go:2477
testing.tRunner(0xADDR, 0xADDR)
$GOROOT/src/testing/testing.go:1934
testing.runTests(0xADDR, {0xADDR, 0xADDR, 0xADDR}, {0xADDR?, 0xADDR?, 0xADDR?})
$GOROOT/src/testing/testing.go:2475
testing.(*M).Run(0xADDR)
$GOROOT/src/testing/testing.go:2337
main.main()
_testmain.go:45
goroutine N [chan receive]:
example.test/lab.TestHangs(0xADDR?)
hang_test.go:16
testing.tRunner(0xADDR, 0xADDR)
$GOROOT/src/testing/testing.go:1934
created by testing.(*T).Run in goroutine N
$GOROOT/src/testing/testing.go:1997
goroutine N [sync.Mutex.Lock]:
internal/sync.runtime_SemacquireMutex(0xADDR?, 0xADDR?, 0xADDR?)
$GOROOT/src/runtime/sema.go:95
internal/sync.(*Mutex).lockSlow(0xADDR)
$GOROOT/src/internal/sync/mutex.go:149
internal/sync.(*Mutex).Lock(...)The command uses -timeout 1s so the failure arrives immediately. Without it, the test would sit for ten minutes.
Reading the dump in four steps
Step 1: the first lines. The output starts with panic: test timed out after 1s, then running tests: and a list of the tests that were running when the timer fired: TestHangs (1s). That names the culprit. If several tests appear, they are running in parallel and any of them may be stuck. If none appears, the time was spent outside a test, in TestMain or in package initialisation.
Step 2: find the test goroutine. The dump lists goroutines separated by blank lines, each with a header like goroutine 23 [chan receive]:. Search for your test function's name. Here, the stack with example.test/lab.TestHangs and hang_test.go:16, headed [chan receive], is the test goroutine: it is blocked receiving from a channel, at line 16, which is the <-done.
Step 3: read the state. The text in square brackets is what the goroutine is waiting for. The common ones:
| State | Meaning |
|---|---|
chan receive, chan send | Waiting on a channel operation |
select, select (no cases) | Blocked in a select, or an empty one that blocks forever |
sync.Mutex.Lock, sync.RWMutex.RLock | Waiting for a lock |
sync.WaitGroup.Wait | Waiting for a group's counter to reach zero |
sync.Cond.Wait | Waiting for a signal |
IO wait, syscall | Waiting for the network or a system call |
sleep | In time.Sleep |
semacquire | Older name for lock and wait group waits |
Our test goroutine is chan receive: it is waiting for someone to send or close.
Step 4: find the other side. Who was supposed to close done? The goroutine started by the test, whose header reads [sync.Mutex.Lock], with a stack through lockSlow and the test's anonymous function at the mu.Lock() line. It is blocked on the mutex. The mutex is held by the test goroutine, which is waiting for this one. Two goroutines, each waiting for the other: the deadlock, in plain view. The line created by testing.(*T).Run in goroutine N and similar lines tell you where each goroutine began.
The runtime's own deadlock detector, which prints fatal error: all goroutines are asleep - deadlock!, does not trigger here, because the testing package's timer goroutine is alive. Under go test a deadlock usually appears as this timeout.
The usual causes
- A channel nobody closes or sends on. A producer that returns on an error and never closes its output, and a consumer ranging over it. The fix is to close the channel with
deferin the producer. - A
WaitGroupwith a wrong count.Addcalled after the goroutine started, orDoneskipped on an error path. Usedefer wg.Done()first thing in the goroutine, and in Go 1.25wg.Go(func() {...}). - A lock taken twice or never released. As in the lab; check every
Lockhas adefer Unlock, and that no function called under a lock takes the same lock. - A goroutine that ignores its context, so a test that cancels and waits for it waits forever.
- A test server that is never closed.
srv.Close()blocks until outstanding requests finish; a handler that waits on something the test holds will hang it. - Network or database calls with no deadline, against a service that is down. Give every external call a context with a timeout.
- A
selectwith no default and no timeout on events that did not happen. - An unbuffered channel send with no receiver, from the test goroutine.
- Real slowness. The suite grew and now exceeds ten minutes honestly. The dump will show a healthy test running, not a blocked one. Raise the limit or speed the suite up; see the slow suite guide.
Set the timeout so you find out sooner
The default of ten minutes is a ceiling for a whole package, and it is too generous for finding hangs. Set -timeout explicitly in CI, a few times the normal duration of the slowest package, such as -timeout 2m. A hang then fails the job in two minutes with the dump, and not in ten with the same dump. Keep the value on the command line or in GOFLAGS, so it applies everywhere.
For one test, give the test its own deadline: derive a context with context.WithTimeout, and wait on select with ctx.Done(). A test that fails with its own message after five seconds is easier to read than a package timeout. The pattern is in the goroutines and channels article and context cancellation tests.
To get the dump without waiting for a timeout, send the test process SIGQUIT (Ctrl and backslash in a terminal): the Go runtime prints every goroutine's stack and exits. That is how you inspect a test that has hung locally and which you do not want to wait for.
Getting more from the dump
GOTRACEBACK=allincludes system goroutines, which is rarely needed. The default is already enough for user code.- Filter by your package. Search the output for your module path; goroutines with only runtime frames are rarely the cause.
- Look for many identical goroutines. Four hundred goroutines blocked at the same line is a leak or a pool that is exhausted, which is a different bug from a deadlock of two.
- Check
created by. The creator frame names the code that spawned a stuck goroutine, which is often where the missing cancel or close belongs. - Use
-raceas well. A hang can be the result of a data race that corrupted a flag, and the detector reports it.
The behaviour of the flag is documented in the testing flags reference under -timeout, and the runtime's goroutine states are listed in the runtime package documentation on tracebacks. For preventing the problem in the first place, see flaky tests.


