Profiling Go services with pprof

Going Deep Dive in Profiling Tools: CPU Bottlenecks and Memory Leaks

Profiling is one of the most underrated yet powerful tools to diagnose performance issues. In this article, I’ll walk you through using Go’s built-in profiling tools to investigate CPU bottlenecks and identify memory leaks via heap profiling.

We’ll also use Apache JMeter to generate realistic load so we can capture meaningful profiling data.

📦 Installing Dependencies

First, make sure you have Go installed (>= 1.18 is recommended).

To install the profiling tool, you don't need any third-party package because Go provides built-in profiling via net/http/pprof. But to visualize the results, Go tool pprof is handy.

1️⃣ Enable pprof

In your Go project, install the module if you haven’t yet:

go mod init your-module-name

Then, import and set up net/http/pprof:

import (
    _ "net/http/pprof"
"log"
"net/http"
)

func init() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}

This exposes profiling endpoints at:

  • http://localhost:6060/debug/pprof/heap
  • http://localhost:6060/debug/pprof/profile
  • http://localhost:6060/debug/pprof/goroutine
  • ...and more.

2️⃣ Install Graphviz (for visualization)

Graphviz is useful for visualizing the output from pprof:

On macOS:

brew install graphviz

On Ubuntu/Debian:

sudo apt-get install graphviz

📝 Example Code to Profile

Here’s a simple Go program that intentionally consumes CPU and memory:

package main

import (
    _ "net/http/pprof"
"log"
"net/http"
"time"
)

func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

    data := [][]byte{}
    for {
        // Simulate CPU work
        go func() {
            for i := 0; i < 1e7; i++ {
            }
        }()

        // Simulate memory leak
        chunk := make([]byte, 1024*1024) // 1MB
        data = append(data, chunk)
        time.Sleep(500 * time.Millisecond)
    }
}

Run the app:

go run main.go

🚦 Load Testing with Apache JMeter

  1. Install JMeter: Download here and unzip it.

  2. Create a Test Plan:

  • Add a Thread Group.
  • Set number of users, ramp-up period, and loop count.
  • Add an HTTP Request Sampler targeting http://localhost:<your-app-port>/your-endpoint.
  1. Start the Test: Hit Run and let JMeter hammer your endpoint while the app is running.

🔍 Capturing Profiles

CPU Profile (30 seconds by default)

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

Heap Profile

go tool pprof http://localhost:6060/debug/pprof/heap

Once in the pprof console, you can type:

top

or

web

(web opens a visual graph if you have Graphviz installed.)


🚨 How to Act on Results

  • High CPU Usage: Look at the top functions consuming the most CPU. If unnecessary goroutines or heavy loops are the culprit, optimize or refactor.

  • Memory Leaks: The heap profile will show allocations that are not being released. Investigate slices, maps, or channels holding on to data longer than needed.

  • Goroutines: Monitor http://localhost:6060/debug/pprof/goroutine?debug=2 to detect goroutine leaks.


✅ Conclusion

With Go’s built-in profiling tools and a bit of pressure testing using JMeter, you can catch performance bottlenecks early. Always act on your profiling data—whether that’s optimizing code, fixing leaks, or rethinking architecture. Profiling isn’t a one-off; it should be part of your regular performance checks.