Growing RSS and suspected memory leaks
A rising resident set size (RSS) in top is not, by itself, proof of a memory leak. First determine whether the growth comes from Go objects, unclosed ImageRef instances, the libvips cache, or native memory retained by the process. Each case needs a different fix.
Symptoms
Section titled “Symptoms”vips.OpenImageRefs()keeps increasing after requests have finished.- The Go heap levels off, but process RSS continues to rise under a steady workload.
- RSS still does not reach a plateau after the libvips cache reaches its configured limits.
- The service is eventually terminated by a memory limit or the OOM killer.
Choose the next measurement
Section titled “Choose the next measurement”| Observation | Likely interpretation | Next step |
|---|---|---|
OpenImageRefs() keeps increasing |
A success or error path omitted Close() |
Audit every branch that creates an ImageRef; add AssertNoLeaks(t) to tests |
| The reference count returns to baseline, but the Go heap grows | Go objects remain reachable, or garbage collection has not run yet | Use a Go heap profile to find retained objects |
| References and the Go heap are stable, but the cache grows | The libvips operation cache has not reached its limit | Set explicit limits in vips.Config, then repeat the measurement |
| All three are stable, but RSS still grows | Allocator fragmentation, decoder memory, and a C-level leak still need to be separated | Collect native-memory evidence under a repeatable workload, then reduce it to a minimal reproducer |
OpenImageRefs() counts unclosed references tracked by govips. It catches a common lifecycle error, but it cannot prove that every other source of native memory is healthy.
Minimal reproduction
Section titled “Minimal reproduction”Reduce the production pipeline to “load one image and close it immediately.” The program below records a baseline after startup and verifies that the reference count returns to it after the loop when run with govips v2.19.0:
package main
import ( "fmt" "log" "os"
"github.com/davidbyttow/govips/v2/vips")
func main() { if len(os.Args) != 2 { log.Fatalf("usage: %s INPUT", os.Args[0]) } if err := vips.Startup(nil); err != nil { log.Fatal(err) } defer vips.Shutdown()
baseline := vips.OpenImageRefs() for i := 0; i < 1000; i++ { image, err := vips.NewImageFromFile(os.Args[1]) if err != nil { log.Fatalf("iteration %d: %v", i, err) } image.Close() }
open := vips.OpenImageRefs() fmt.Printf("open ImageRef instances: baseline=%d after=%d\n", baseline, open) if open != baseline { log.Fatalf("ImageRef count did not return to baseline") }}If this stays stable while the full pipeline grows, add loading, transformation, and export stages back one at a time. This finds the first stage that fails to return to baseline instead of guessing from restarts or forced garbage collection.
Fix in this order
Section titled “Fix in this order”1. Make ImageRef ownership explicit
Section titled “1. Make ImageRef ownership explicit”Every successfully created ImageRef needs one clearly responsible code path that calls Close(). Check normal returns as well as decode errors, validation failures, export errors, cancellation, and other early returns. Do not rely on a finalizer in place of explicit release.
Tests can use defer vips.AssertNoLeaks(t) to fail when a test finishes with open references. It is a regression check, not a replacement for production monitoring.
2. Verify process-level lifecycle
Section titled “2. Verify process-level lifecycle”Call vips.Startup(...) once during process initialization and check its error. Call vips.Shutdown() only after image-processing work has stopped. Do not start and stop libvips for every request.
3. Bound the cache before comparing RSS
Section titled “3. Bound the cache before comparing RSS”govips v2.19.0 exposes MaxCacheSize, MaxCacheMem, and MaxCacheFiles through vips.Config. Set explicit limits for the experiment, then repeat the same workload and check whether RSS reaches a plateau. vips.PrintCache() can aid diagnosis; vips.ClearCache() should remain a diagnostic tool because frequent cache clearing changes normal performance behaviour.
4. Record the Go heap and process RSS separately
Section titled “4. Record the Go heap and process RSS separately”A Go heap profile covers Go-managed objects. RSS also includes libvips, codecs, thread stacks, mapped files, and pages retained by the native allocator. Only after the reference count, Go heap, and libvips cache have stabilised does continued unbounded RSS growth justify investigating the allocator or a C-level leak.
Affected versions and scope
Section titled “Affected versions and scope”- This page targets govips v2.19.0 and the repository’s current libvips 8.17.3 baseline.
OpenImageRefs()andAssertNoLeaks(t)are available from govips v2.18.0; older releases cannot use this step directly.- sharp, pyvips, and the vips CLI have different object lifecycles and diagnostics. Do not copy the govips reference-count method to them unchanged.