A server recently stopped responding. After digging in, I traced the trigger to a zip upload and extraction feature in one of its web services. The code didn't make it obvious which step was ballooning memory, so I turned to pprof. Here's how the investigation went.
Enabling pprof
Pull in pprof and expose an HTTP service on port 6060 (a common convention, not a hard requirement).
package main
import (
"net/http"
_ "net/http/pprof"
// ...
)
func main() {
go func() {
// pprof server, will be exposed on port 6060
if err := http.ListenAndServe(":6060", nil); err != nil {
panic(err)
}
}()
// ...
}
Once the program is running, open http://localhost:6060/debug/pprof/ to see what can be captured — CPU, memory, and more.
Profiling memory
I was after memory usage, so I chose the heap profile. During this window I manually hit the endpoint to reproduce the high-memory moment.
# Fetch performance data
curl -o heap.pprof 'http://localhost:6060/debug/pprof/heap'
Then I copied the resulting heap.pprof down to my local machine.
Visual analysis
The web visualization relies on graphviz, so install it first. On macOS I used brew:
$ brew install graphviz
Then run the analysis against the captured profile:
$ go tool pprof -http=:8081 heap.pprof
The command opens a browser automatically. A few screenshots:

Something doesn't add up
The zip was only 32MB, and even after extraction it was 75MB.

There was no reason for it to consume nearly 600+MB of memory...
To keep verifying, I installed gnu-time on macOS. Unlike the built-in time, gtime reports peak memory usage during a run.

Extracting quietly showed that the unzip tool itself uses very little memory — only 3664KB.
My suspicion fell on the Unzip function I'd previously taken from an AI suggestion, which likely had a performance problem.
Testing the hypothesis
Here's the code:
func Unzip(src string, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
fileInArchive, err := f.Open()
if err != nil {
return err
}
defer fileInArchive.Close()
path := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
_, err = io.Copy(f, fileInArchive)
if err != nil {
return err
}
}
}
return nil
}
The caller:
main.go
func main() {
err := utility.Unzip("assets.zip", "/tmp/unzip-folder")
if err != nil {
fmt.Println(err)
}
}
Build and run:
$ go build -o unzip-test main.go
# Run
$ gtime ./unzip-test

It really did consume 728528 KB. Once the problem is pinned to a specific function, fixing it becomes much easier.
The fix
Looking through the code, I found the issue: the snippet below uses defer fileInArchive.Close() inside the loop to close files. But defer only fires when the function returns — it doesn't run per iteration.
for _, f := range r.File {
fileInArchive, err := f.Open()
if err != nil {
return err
}
defer fileInArchive.Close()
// ...
}
The fix is to close files directly inside the loop.
func Unzip(src string, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
fileInArchive, err := f.Open()
if err != nil {
return err
}
path := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
_, err = io.Copy(f, fileInArchive)
if err != nil {
return err
}
f.Close()
fileInArchive.Close()
}
}
return nil
}
Retested:

Memory usage dropped to 35.5MB, right where it should be. Problem solved.
Takeaways
- pprof is built into Go services — import it and expose an HTTP endpoint to use it. Just don't expose it in production.
- Using defer to close files inside a loop is a classic mistake worth watching for.
- AI-provided code is easy to get wrong. Code review and unit tests are not optional.