How to Get the Current Number of Running Goroutines

Get the current number of running goroutines in a Go program using the runtime.NumGoroutine() function.

Story Opener

Your HTTP server is eating memory. The CPU usage is flatlining, but response times are creeping up. You suspect a goroutine leak: a background task that started but never finished, piling up until the scheduler chokes. You check the logs. Nothing obvious stands out. You need a quick way to count how many goroutines are alive right now. Go gives you a single function for this: runtime.NumGoroutine(). It is the first line of defense when your concurrency model starts to wobble. You call it, you get a number, and you start asking why that number is higher than it should be.

Concept: The Scheduler's Headcount

Think of runtime.NumGoroutine() as a headcount in a busy kitchen. The chef calls out, "How many cooks are on the floor right now?" The answer is a number. It includes the head chef (the main goroutine), the line cooks (your workers), and even the dishwasher if they are still in the room. The number tells you the total load on the scheduler. It does not tell you what they are doing. It does not tell you if they are stuck. It just tells you how many exist.

In Go, goroutines are lightweight. You can spawn thousands without breaking the bank. The count helps you verify that your lightweight tasks are actually finishing. If the count climbs and never drops, your lightweight tasks have become heavy anchors. The function returns an integer representing the total number of goroutines the runtime knows about at that moment.

Minimal Example

Here is the simplest call: import the runtime package, call the function, and print the result.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// runtime.NumGoroutine counts all goroutines, including the main one.
	count := runtime.NumGoroutine()
	fmt.Println("Goroutines:", count)
}

The output will be at least one. The main goroutine always exists. Your editor runs gofmt on save, so the indentation in these examples matches the tool's output. Trust gofmt. Argue logic, not formatting.

Walkthrough: How the Count Works

When you call runtime.NumGoroutine(), the Go runtime pauses its internal bookkeeping just enough to count the active goroutine objects. The scheduler maintains a list of all goroutines. This function walks that list and returns the length. The call is fast, but it is not instantaneous. If a goroutine spawns or exits while the count is happening, the result might shift by one. The function returns the number it calculated at that moment. It does not block the program. It does not stop the world.

The runtime uses atomic operations and internal locks to protect the count. You get a consistent snapshot relative to the moment the function returns. The number is accurate for that instant. It is not a promise about the next millisecond. The function is safe to call from any goroutine. You can call it from a worker, from the main function, or from a signal handler. The runtime handles the concurrency of the count itself.

Realistic Example: Monitoring a Burst

In a real application, you usually check the count to detect leaks or monitor load. Here is a pattern where you log the count before and after a burst of work. This simulates a server handling a spike in requests.

package main

import (
	"fmt"
	"runtime"
	"time"
)

// doWork simulates a task that takes time.
func doWork(id int) {
	// Sleep to simulate I/O or computation.
	time.Sleep(10 * time.Millisecond)
}

func main() {
	// Baseline count includes the main goroutine.
	fmt.Println("Before:", runtime.NumGoroutine())

	// Spawn ten goroutines to simulate concurrent requests.
	for i := 0; i < 10; i++ {
		go doWork(i)
	}
}

The code spawns ten workers. In production, you would pass a context.Context as the first parameter to doWork to allow cancellation. Functions that take a context should respect deadlines. Here we keep it simple to focus on the count.

// ... inside main after spawn ...

	// Wait briefly so the goroutines have time to start.
	time.Sleep(20 * time.Millisecond)

	// Count should be higher now: main plus the ten workers.
	fmt.Println("During:", runtime.NumGoroutine())

	// Wait for workers to finish.
	time.Sleep(50 * time.Millisecond)

	// Count drops back to baseline once goroutines exit.
	fmt.Println("After:", runtime.NumGoroutine())
}

The output shows the count rising and falling. If the "After" count is higher than the "Before" count, something leaked. The time.Sleep calls are only for demonstration. Real code uses synchronization primitives like sync.WaitGroup or channels to coordinate. The count is a metric, not a control mechanism.

What Counts as a Goroutine?

The count includes everything. It includes the main goroutine. It includes goroutines spawned by the runtime itself. The garbage collector runs in its own goroutines. Finalizers run in goroutines. If you have a lot of system goroutines, the count might be higher than you expect. This is normal. The runtime manages its own workers. You do not need to subtract them. The number represents total pressure on the scheduler.

You cannot filter the count. There is no runtime.NumUserGoroutine(). The function returns the total. If you are debugging a leak, you look for a count that grows over time and never stabilizes. A stable count that is higher than zero is fine. A rising count is a problem. The count measures concurrency, not parallelism. You can have a million goroutines on a single CPU core. The scheduler multiplexes them. The count goes up, but the CPU usage might stay low if they are all waiting on I/O. A high count with low CPU often means blocked goroutines. A high count with high CPU means parallel work. The number alone does not distinguish. You need to look at CPU profiles too.

Pitfalls: The Count Is Not a Lock

The biggest trap is treating the count as a synchronization tool. You cannot wait for goroutines by polling runtime.NumGoroutine() until it hits zero. The count is a snapshot. A goroutine might exit between the check and the next iteration, or a new one might spawn. The number is an approximation. If you try to use this for coordination, you will get flaky behavior. The count might drop, but a goroutine could still be running. Or the count might stay high because a goroutine is blocked on a channel that never sends.

The compiler will not stop you from using the count for control flow. It returns an int. You can put it in an if statement. The runtime does not care how you use the number. The bug appears at runtime when your logic assumes the count is stable. Also, remember the count includes the main goroutine. If you expect zero, you will see at least one. System goroutines also count. You cannot isolate the count to a specific set of workers. The count is global.

Calling NumGoroutine is cheap, but it is not free. It requires the runtime to iterate over internal structures. If you call it in a tight loop, you add overhead. The function is safe to call from any goroutine. It does not require a lock that blocks the whole program. However, calling it thousands of times per second in a hot path is a bad idea. Use it for periodic checks, not for per-request accounting. In tests, you might want to assert that no goroutines leaked. You can check the count at the start and end of a test. If the count is higher at the end, something leaked. Be careful: other tests might run in parallel. The count is global. You cannot isolate the count to a single test. Use a baseline and check for deltas, or run tests sequentially if you rely on the count.

Diagnosing Leaks with the Count

When you see the count rising and never falling, you have a leak. A goroutine leak happens when a goroutine blocks forever. Common causes include waiting on a channel that never sends, holding a mutex that never releases, or reading from a connection that never closes. NumGoroutine tells you the leak exists. It does not tell you where. To find the leak, you need stack traces. The pprof tool can dump goroutine stacks. You can also use runtime.Stack to print stacks manually. The count is the alarm. The stack trace is the map.

Goroutine leaks are insidious. They do not crash the program immediately. They slow it down. The scheduler spends time managing idle goroutines. Memory usage grows. Eventually, the system runs out of resources. The worst goroutine bug is the one that never logs. It just sits there, consuming resources until the server dies. Regularly checking the count in your metrics can catch leaks early. Set up an alert if the count exceeds a threshold. The threshold depends on your application. A web server might have hundreds of goroutines under load. A background worker might have dozens. Know your baseline.

Decision: When to Use What

Use runtime.NumGoroutine() when you need a quick snapshot of scheduler load for logging or metrics. Use sync.WaitGroup when you need to wait for a specific set of goroutines to finish before proceeding. Use a channel with a done signal when you need to coordinate a single result or cancellation between goroutines. Use pprof or the net/http/pprof endpoint when you need to inspect goroutine stacks to find leaks, not just the count. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.

The count is a metric. Synchronization is a mechanism. Keep them separate. Don't poll the count. Use WaitGroup.

Where to go next