byterot.blogspot.com/201...

image from Daniel Ismail’s linkedin post

sEver had a backend service that ran fine in staging but started choking after a few days in production? Maybe memory usage kept creeping up until it crashed, got OOM-killed, or just slowed to a crawl. That, my friend, might’ve been a memory leak.

And no, this isn’t just a C or C++ thing. Memory leaks absolutely happen in managed languages too. They just look different. Instead of crashing instantly, they lurk in the background, quietly eating RAM until your system taps out.

Let’s talk about what memory leaks really are, how they sneak into backend systems, and how you can catch them before they take your app down.


So What Exactly Is a Memory Leak?

A memory leak happens when your app holds onto memory it doesn’t need anymore. In languages like Go, Java, Python, or Node.js, this usually means the garbage collector can’t clean it up. Not because it’s in use, but because your code is still referencing it somewhere.

The result? Your memory usage grows slowly and silently. No errors. No crashes. Just pain later.


How Memory Leaks Show Up in Real Life

1. Unbounded In-Memory Caches

You thought you were being smart by caching DB results in memory. But you forgot to set a size limit or an eviction strategy. Now your “fast” cache is slowly hoarding everything it’s ever seen.

Looks like: Memory usage goes up over time, even when traffic is stable.
Fix: Add a TTL, max size, or use an LRU cache.


2. Async Stuff That Never Finishes

Unresolved promises, futures, or goroutines can pile up if something in your code doesn’t clean them up properly. Like a missing timeout or retry loop gone wrong.

Looks like: Memory grows with load. Sometimes you’ll also notice event loop lag or thread pool saturation.
Fix: Set timeouts, use cancellation, and make sure async jobs actually finish.


3. Forgetting to Unsubscribe

Ever add event listeners to a message bus, WebSocket, or emitter and then forget to remove them? Now your app is holding onto stale connections, references, and who-knows-what.

Looks like: Memory grows with each new user or connection. Node.js might even warn you about it.
Fix: Always unsubscribe. Clean up when connections close or components unmount.


4. Logging Gone Wild

Logging entire request payloads in memory before shipping them off? Keeping debug logs in production? Yeah... don’t do that.

Looks like: High heap usage, especially when traffic spikes.
Fix: Use async, non-blocking logs. Don’t log full payloads unless you really need to.


5. Global Variables Hoarding Data

Sometimes it’s tempting to store things in global maps, static fields, or singletons. Until they start storing everything and never let go.

Looks like: Memory grows with every user, tenant, or request.
Fix: Keep global state small and clean up after use. Use scoped containers or request-specific storage.


How to Spot a Leak Before It’s Too Late

Heap Snapshots Are Your Friend

Take a memory snapshot when things seem normal. Take another one later under load. Compare. If you see objects that keep growing and never go away, there’s your leak.

Tools

  • Node.js: Chrome DevTools, Heapdump
  • Go: pprof
  • Java: VisualVM, Eclipse MAT
  • Python: tracemalloc, guppy

Monitor Memory in Production

Don’t just wait for OOM kills. Set up dashboards and alerts on:

  • Heap size
  • GC pauses
  • RSS usage
  • Container memory limits

Pro tip: Memory that spikes and drops is usually fine. Memory that climbs slowly and never drops is a red flag.


Long-Running Load Tests

Most memory leaks don’t show up in a 5-minute load test. Try running your test for 30 to 60 minutes at steady load. Watch what happens to memory.

Tools: k6, wrk, artillery, locust


Use Limits as a Safety Net

In containers or Kubernetes? Set memory limits. You’ll at least get an early warning (OOMKilled) instead of mysterious slowness that ruins your weekend.

Also, set alerts when memory crosses 80 to 90 percent of that limit.


Watch for the Patterns

During code reviews, look for usual suspects:

  • Caches without limits
  • Event listeners with no unsubscribe
  • Background jobs with no timeouts
  • Global maps growing forever

Leaks often come from small, easy-to-miss mistakes.


What You Can Do Right Now

  • Add memory usage charts to your dashboards
  • Set memory limits and alerts in your orchestrator
  • Check if your in-memory caches have size or TTL
  • Run a long-duration load test
  • Take a heap snapshot and see what’s sticking around
  • Audit teardown logic and async task cleanup

Wrapping Up

Memory leaks aren’t loud. They don’t crash your app on day one. They wait. Then they hit when traffic spikes, when you’re on vacation, or when you’re demoing to a customer.

If your backend is using more memory than it should, and you’re not sure why, it might be leaking.

Catch it early. Clean it up. And remember, just because your code runs doesn’t mean it lets go.