goleaked

command module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 6 Imported by: 0

README

🕵️ goleaked

Point it at a running Go process, catch the goroutines that never finish

Go version Dependencies Status License

A dependency-free CLI that attaches to a running Go process over net/http/pprof, samples its goroutines repeatedly on a timer, and flags the stack traces that keep recurring, sample after sample, without ever finishing. That recurrence, not a single snapshot, is the actual signature of a goroutine leak.

$ goleaked -url http://localhost:6060/debug/pprof/goroutine?debug=2 -duration 5m -interval 10s

goleaked: sampled 30 times over 5m0s (interval 10s)

found 1 possible leak(s):

[1] state: chan receive, 4 minutes
    main.handleUpload()
        /app/upload.go:42 +0x30
    created by main.main
        /app/main.go:88 +0x24

What it is

A small, single-binary pipeline with four stages: fetch a goroutine dump over HTTP from a target process's pprof endpoint, parse the raw text into structs, sample it repeatedly on a timer, and diff the accumulated samples by hashing each goroutine's state + stack into a signature and counting how many distinct samples each signature shows up in. A signature present in a high enough fraction of samples (configurable, not hardcoded) gets flagged as a likely leak. An allowlist filters out stacks you already know are supposed to run forever (an HTTP server's own accept loop, for instance), so a legitimately permanent background goroutine doesn't get flagged alongside an actual bug.

What it solves

Go's built-in tools give you the pieces, but not this specific answer, without extra effort:

  • go tool pprof / a single curl of /debug/pprof/goroutine gets you one snapshot. A snapshot alone can't tell you whether a stuck goroutine is a permanent, healthy part of the program or an actual leak that's about to take the process down: you'd have to pull several dumps by hand and diff them yourself to find out.
  • uber-go/goleak, the well-known library in this space, solves a different problem: it's a single before/after comparison run inside a test, asserting no unexpected goroutine survived past t.Cleanup. It doesn't watch a live, running process over time.
  • Continuous profiling platforms (Grafana Pyroscope, Google Cloud Profiler, APM vendor add-ons) do sample a live process over time, but they're built for a human to browse a flame graph and notice something looks wrong, not to automatically hash-and-diff recurring stacks and hand back a yes/no list of suspects.

goleaked sits in the gap: point it at a suspicious process for a few minutes and get a short, concrete list of candidates, without wiring up a test or standing up an observability platform.

How it works

stage package does
fetch internal/pprofclient HTTP GET the pprof goroutine dump (?debug=2) from a target URL
parse internal/parse turn the raw text dump into []Goroutine{ID, State, Stack} structs
sample internal/sampler poll fetch+parse on a timer, accumulate timestamped samples behind a mutex
diff internal/leak hash each goroutine's state+stack into a signature, count which signatures recur across samples, filter via an allowlist, flag anything past a threshold

Correctness has been verified against testtarget/, a small local server with two goroutines deliberately stuck forever, confirming goleaked finds exactly those two and nothing else.

Install

go install github.com/Aparajith24/goleaked@latest

Or build from source:

git clone https://github.com/Aparajith24/goleaked.git
cd goleaked
go build -o goleaked .
./goleaked -url http://localhost:6060/debug/pprof/goroutine?debug=2

Or run it directly without a separate build step:

go run . -url http://localhost:6060/debug/pprof/goroutine?debug=2

Usage

goleaked [flags]

  -url string
        pprof goroutine dump URL (default "http://localhost:6060/debug/pprof/goroutine?debug=2")
  -interval duration
        how often to sample (default 1s)
  -duration duration
        how long to sample for (default 5s)
  -threshold float
        fraction of samples a signature must appear in to be flagged, 0-1 (default 1)
  -ignore string
        comma-separated substrings of stacks to ignore (default "net/http.(*Server).Serve")

The target process just needs _ "net/http/pprof" blank-imported and an HTTP server running somewhere reachable, nothing else to install on the target side.

Who this is for

  • You have (or suspect) a goroutine leak in a running Go service and want a quick, concrete list of candidate stacks, without writing a test or reaching for a full profiling platform.
  • You're comfortable running a short-lived CLI against a process's pprof endpoint for a few minutes and reading a stack trace to figure out the fix yourself. goleaked finds candidates, it doesn't fix anything or explain why a stack is stuck.

What this is not

  • Not a replacement for uber-go/goleak in tests. If you want a CI-enforced "no leaked goroutines" assertion at the end of a test run, that's a different, already-solved problem: use goleak.
  • Not a production monitoring / always-on tool. There's no persistence, no alerting, no dashboard, no long-term storage of samples across runs. It's a point-in-time diagnostic CLI you run when you already suspect a problem, not something you'd leave running against production indefinitely.
  • Not a fix, and not root-cause analysis. It tells you which stack keeps recurring, not why; that part is still on you.
  • Not aware of process restarts. Goroutine IDs and all in-memory sample state are scoped to one continuous goleaked run against one continuously-running target process. If the target restarts mid-run, samples from before and after the restart get compared as if they were the same process.

Known limitations

  • The allowlist is a plain substring match (strings.Contains) against the full signature text, not a structured pattern language. Effective for filtering out known infrastructure frames like an HTTP server's accept loop, but easy to over-match if a pattern happens to appear in an unrelated stack too.
  • A goroutine that's merely slow, not leaked, can still get flagged if it happens to be running long enough to span every sample in a short observation window (e.g. -duration 5s with a task that legitimately takes 6s). Longer -duration values relative to your program's normal worst-case latency reduce this.
  • No goroutine count/growth tracking. A signature that shows one stuck instance across every sample and a signature whose instance count is climbing every sample look the same to the current threshold logic: both just get counted as "present in this sample or not." Distinguishing a single permanently-stuck worker from an actively growing leak is a natural next step.

Built to learn Go

goleaked was built as a hands-on way to actually learn Go: goroutines, channels, sync.Mutex/sync.WaitGroup, the runtime scheduler, and net/http/pprof, not just to end up with a working binary. Each piece was worked through by hand before moving to the next, most notably the concurrency layer: deliberately reproducing a data race with go run -race, fixing it with sync.WaitGroup and sync.Mutex, then carrying that same discipline into the sampling loop, where a background goroutine polls on a timer while the main program reads accumulated results safely through a mutex. Parsing the raw pprof dump was written by hand with strings/bufio rather than reached for via a regex library, to actually understand what a parser does. The signature-and-recurrence logic, threshold, and allowlist all came out of watching a real false positive happen (an HTTP server's own accept loop getting flagged as a "leak") and fixing it, not from designing it upfront.

License

MIT, see LICENSE.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL