Loading Raymanthis…

Engineering

Debugging a memory leak in a Next.js Server Component

Daniel Akhile1 min read
Placeholder image for a case study

The symptom was a Node process that climbed from 300MB to 1.4GB over about six hours and then got killed by the container runtime. No deploy correlated with it, and no single route looked guilty.

It turned out to be a closure holding a reference to every request it had ever served. Finding it took a heap snapshot and an afternoon of patience.

#Reading the heap snapshot

We took two snapshots an hour apart under steady traffic and diffed them. The retained size column pointed at a Map that only ever grew, keyed by request URL.

Following the retainers back led to a module-level cache we added to avoid recomputing a navigation tree. It looked harmless because the tree itself was small.

  • Take two snapshots, not one, and diff them
  • Sort by retained size, then follow the retainer chain upward
  • Look for module-level Maps, Sets and arrays first

#The one-line fix

The cache key included the full search string, so every unique query produced a new entry that was never evicted. Tracking parameters alone made the key space effectively infinite.

We keyed on the pathname only and bounded the cache. The memory line has been flat for three weeks.

ts
// before: unbounded, keyed by the full URL
const navCache = new Map<string, NavTree>()

// after: keyed by pathname, bounded
const navCache = new Map<string, NavTree>()
const MAX_ENTRIES = 200

export function getNav(pathname: string): NavTree {
  const hit = navCache.get(pathname)
  if (hit) return hit

  const tree = buildNavTree(pathname)
  if (navCache.size >= MAX_ENTRIES) {
    navCache.delete(navCache.keys().next().value!)
  }
  navCache.set(pathname, tree)
  return tree
}

Share

Daniel Akhile

Written by

Daniel Akhile

Director of Engineering

Daniel leads engineering at Raymanthis.

Meet the team →

Building something similar?