Skip to content

Growing RSS and suspected memory leaks

Copy page

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.

  • 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.
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.

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.

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.

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.

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.

  • This page targets govips v2.19.0 and the repository’s current libvips 8.17.3 baseline.
  • OpenImageRefs() and AssertNoLeaks(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.