Open-source project
sasha-s/go-deadlock avatar
sasha-s/go-deadlock

go-deadlock, a mutex replacement that reports the lock order it saw

Online deadlock detection in go (golang)

1,196 stars80 forksGoApache-2.0

At a glance

What is it?
go-deadlock provides drop-in replacements for sync.Mutex and sync.RWMutex that detect two specific classes of deadlock at runtime: inconsistent lock ordering between goroutines, and taking the same lock twice in one goroutine. When it sees either, it prints a report and exits, and it also reports a lock that has been held for too long with the stack trace of the holder.
Who is it for?
Adopt go-deadlock in a test environment or a staging deployment where a crash and a stack trace are acceptable, and skip it in a production hot path where a panic and a process exit are not, because that is what the detection does by default.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 98 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • sasha-s/go-deadlock1,196 ★ · -1
Star history
Repository2026-10-202026-11-07
sasha-s/go-deadlock1.2k1.2k

-1 Stars in the last 18 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

A drop-in replacement, with one important limit

The library provides (RW)Mutex drop-in replacements for sync.(RW)Mutex, and the usage is deliberately unremarkable. You import the package, declare a deadlock.Mutex or a deadlock.RWMutex instead of the standard one, and use it exactly as you would use the standard type, locking and deferring the unlock. The swap is a type name change and an import. The limit is stated in one line and it is the one thing to understand before you install the dependency: it would not work if you create a spaghetti of channels, and the scope is mutexes only. That is a narrower claim than some deadlock detectors make, and it is honest, because the detection works by observing lock attempts on a specific mutex from a specific goroutine, and a channel operation leaves no such trace. If your contention is in channels, this tool will not see it.

go
import "github.com/sasha-s/go-deadlock"
var mu deadlock.Mutex
// Use normally, it works exactly like sync.Mutex does.
mu.Lock()

defer mu.Unlock()
// Or
var rw deadlock.RWMutex
rw.RLock()
defer rw.RUnlock()

Inconsistent lock ordering, the classic deadlock

The most common source of deadlocks is inconsistent lock ordering, and the project explains it with a two-goroutine example. In one goroutine you take lock A and then lock B, and in another the order is reversed, B then A. The tool detects this because each time it sees a lock attempt for lock B, it records the order A before B for each lock currently held in the same goroutine, and it prints, and exits the program by default, when it sees the locking order being violated. The detection works even if the deadlock itself happens very infrequently and is painful to reproduce, which is the case that matters, because a deadlock that reproduces on demand does not need a tool. The report is what you would want: it shows the ordering seen in one goroutine with the source lines marked, and the reverse ordering seen in another, so the fix is obvious.

go
A.Lock() // defer A.Unlock() or similar.
...
B.Lock() // defer B.Unlock() or similar.

Taking the same lock twice, which the tool also catches

The second source of deadlocks the library handles is taking a lock twice in the same goroutine, whether it is a read lock followed by another read lock or a write lock followed by a read lock. The documentation is careful about the strength of the claim: this does not guarantee a deadlock, because maybe the goroutines above can never be running at the same time, but it usually is a design flaw at least. That caveat matters for how you read a report, and it also matters for what you do next, because the fix for a reported ordering violation is often to restructure the critical section rather than to reorder the locks. There is a limit to the detection here too, and the project states it plainly: unless you cross a goroutine boundary, for example lock A, then spawn a goroutine, block until it signals, and lock B inside the goroutine. The tool can detect such cases, but only when the locks are held in the same goroutine's view.

go
A.Rlock() or lock()

A.lock() or A.RLock()

The long wait report, which is the other half of the tool

Independently of the ordering analysis, there is a timeout detector. If it sees that the program is waiting on a lock for a long time, with opts.DeadlockTimeout at 30 seconds by default, it reports a potential deadlock, also printing the stack trace for a goroutine that is currently holding the lock being waited on. The sample output for this case is worth reading because it is the artefact you will be staring at. It shows the previous place where the lock was grabbed with the goroutine and source line, how long the current attempt has been trying, the goroutine that is trying, and then what the holding goroutine is doing right now, with a full stack trace. That last part is what makes it useful rather than merely alarming, because a deadlock report that only says which lock is stuck leaves you to work out the rest, while one that prints the holder's stack shows you the cycle. The default of 30 seconds also means the tool will not fire on a fast deadlock, and the ordering detection is what catches those.

code
POTENTIAL DEADLOCK:
Previous place where the lock was grabbed
goroutine 240 lock 0xc820160440

Have been trying to lock it again for more than 40ms
goroutine 68 lock 0xc820160440

The build modes, and how to turn the detection off

The project supports multiple build configurations for different use cases, and the list starts with normal mode as the default, which is full deadlock detection with timer pooling. Alongside it there is a synctest mode, and the file names in the repository show the shape of the implementation. There are files suffixed for go124 and for legacy behaviour, including separate timer pool files for the default, the disabled build, go125 and synctest, and a sync_disable file with a go118 variant, which is the pattern for excluding the standard library's own sync package under certain build constraints. The effect of the disable mode is not that the tool misbehaves but that it becomes the standard library mutex, which is how you would ship a build with the detection compiled out while keeping the source unchanged. There is also a test.sh and a buildtag_test.go, so the mode matrix is itself tested rather than asserted.

code
sync_disable.go
sync_disable_go118.go
timerpool_default.go
timerpool_disable.go
timerpool_go125.go
timerpool_synctest.go

The recursive read lock, and why it deadlocks

There is a separate section on grabbing an RLock twice from the same goroutine, and the opening line is that this is, surprisingly, not a good idea. The RWMutex documentation is quoted to explain why: if a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released, and in particular this prohibits recursive read locking, to ensure that the lock eventually becomes available, because a blocked Lock call excludes new readers from acquiring the lock. The example that follows is a runnable program that deadlocks, and it is offered both on the standard playground and on wandbox with go-deadlock enabled, which is a nice touch because it lets you see the difference the tool makes. A related project exists for a mutex that works with net.context, which is a reminder that the standard library's sync package has no context-aware variant.

go
var mu sync.RWMutex

rlockTwice := func() {
	mu.RLock()
	fmt.Println("first Rlock succeeded")
	mu.RLock()
	fmt.Println("second Rlock succeeded")
	mu.RUnlock()
	mu.RUnlock()
}

Where it has already caught something

The project lists two real uses, and both are in large Go codebases rather than in demos. One is a potential deadlock between Gossip.SetStorage and Node.gossipStores in CockroachDB. The other is a race between garbage collection and row mutations in the bigtable test harness, and that second one is the more interesting reference because it is a deadlock found by a test suite rather than by a user report, and because the sample output in the documentation is taken from exactly that code, with the inmem.go and inmem_test.go line numbers visible in the report. That is the case the tool is built for: a lock order that is wrong but that almost never deadlocks in practice, where the race window is small and the reproduction is painful. The go.mod file is minimal, requiring Go 1.17 and a single dependency, goid, which is what the tool uses to identify a goroutine. The licence is Apache 2.0, and the release history shows patch-level versions through 0.3.x with the most recent visible release being v0.3.9 in March 2026.

Editorial conclusion

Adopt go-deadlock in a test environment or a staging deployment where a crash and a stack trace are acceptable, and skip it in a production hot path where a panic and a process exit are not, because that is what the detection does by default. Verify three things before you deploy it: that your locking is genuinely mutex-based, since the library states it would not work if you create a spaghetti of channels; that the timeout you configure matches your slowest legitimate critical section, since a 30 second default will not fire on a fast deadlock and a short one will fire on a slow operation; and which build configuration you select, since the project ships several modes including a disable mode and a synctest mode for the newer testing facilities.

Frequently asked questions

What does go-deadlock detect?

Two classes of deadlock. The first is inconsistent lock ordering, where one goroutine takes lock A then B and another takes B then A, which it detects by recording the order of locks held in the same goroutine and reporting when that order is violated. The second is taking the same lock twice in one goroutine, whether a read lock followed by a read lock or a write lock followed by a read lock.

How do I install and use go-deadlock?

Run `go get github.com/sasha-s/go-deadlock/...`, import the package, and use deadlock.Mutex or deadlock.RWMutex in place of the standard types. They work exactly like sync.Mutex and sync.RWMutex, so the rest of the code does not change.

What happens when go-deadlock detects a problem?

It prints a report and exits the program by default. For the ordering violation the report shows the ordering seen in one goroutine and the reversed ordering in another, with source lines. For a long wait it prints the stack trace of the goroutine currently holding the lock, alongside the goroutine trying to take it.

What are the limits of go-deadlock?

It would not work if you create a spaghetti of channels, and it handles mutexes only. The ordering detection also fails when you cross a goroutine boundary, for example locking A, then spawning a goroutine, blocking until it signals, and locking B inside that goroutine. The default timeout is 30 seconds, so it will not fire on a fast deadlock.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. sasha-s/go-deadlock on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sasha-s-go-deadlock.svg)](https://hysenlabs.com/projects/sasha-s-go-deadlock)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/sasha-s-go-deadlock.svg)](https://hysenlabs.com/projects/sasha-s-go-deadlock)

Was this useful?