Learning how to debug a memory spike.

•

•

Memory Doubled in Ten Seconds

I don’t care how many expensive observability platforms your company just signed a contract for; no dashboard is going to magically tell you how to debug a memory spike when the heap is already redlining. I’ve seen teams spend three days chasing “phantom leaks” in their application code, only to realize the issue was a misconfigured connection pool or a massive, unbuffered file read that shouldn’t have happened in the first place. Most of the “expert” advice out there is just a collection of buzzwords that fall apart the moment you’re staring at a crashing pod in the middle of a Tuesday night.

I’m not here to sell you on a specific tool or a trendy new monitoring stack. Instead, I want to walk through the actual, unglamorous steps I take when the alerts start screaming. I’ll show you how to look at the telemetry that actually matters, how to find the specific objects that are hogging your RAM, and—most importantly—how to distinguish a genuine leak from a simple case of lazy garbage collection. This is about the practical, messy reality of keeping a system upright.

Table of Contents

Monitoring System Resource Usage Before Everything Crashes

Monitoring System Resource Usage Before Everything Crashes

Before you start digging through heap dumps, you need to know what “normal” actually looks like. I’ve learned the hard way that you can’t spot an anomaly if you don’t have a baseline. Most of my recent incidents started not with a sudden crash, but with a slow, creeping slope in the telemetry. If you aren’t monitoring system resource usage with enough granularity to see the trend lines over a 24-hour period, you’re essentially flying blind. You want to see the sawtooth pattern of the heap—the steady climb followed by a sharp drop when the collector kicks in. If those valleys aren’t getting lower over time, you aren’t looking at a temporary load spike; you’re looking at a slow death.

I usually keep a dashboard open that tracks more than just raw RAM. I look for the relationship between request throughput and memory consumption. If your memory usage climbs even when traffic is flat, you’ve likely moved past simple scaling issues and into the territory of detecting excessive object allocation or, more likely, a leak. Don’t just wait for the OOM killer to show up and end the conversation. Set up your alerts to trigger when the rate of increase looks suspicious, not just when you hit a hard threshold. By the time you hit 90% utilization, the decision-making window has already closed.

Identifying Memory Leaks in Applications While They Bleed

Identifying Memory Leaks in Applications While They Bleed

Once you’ve confirmed the metrics aren’t just a temporary burst of traffic, you’re likely looking at a slow, steady climb that never settles. This is where the real work starts: identifying memory leaks in applications while they are actively bleeding resources. I’ve learned the hard way that watching a graph go up is easy; understanding why the memory isn’t returning to the baseline is the part that keeps you awake. You aren’t looking for a single massive allocation; you’re looking for the thousands of tiny, forgotten objects that the garbage collector simply can’t reclaim because something, somewhere, is still holding a reference to them.

At this stage, I usually stop guessing and start pulling data. This is where heap dump analysis techniques become my primary tool. Instead of staring at a dashboard, I’ll grab a snapshot of the heap and look for the “path to GC roots.” I want to see which specific class or collection is acting as a black hole, swallowing objects and refusing to let them go. It’s rarely a complex architectural failure; more often than not, it’s a static Map that someone used as a temporary cache and forgot to clear, or a listener that was never unhooked. It’s tedious, manual work, but it’s the only way to move from “the server is dying” to “here is the exact line of code causing the leak.”

Five things I actually do when the alerts start screaming

  • Stop looking at the code and start looking at the garbage collector. If you see a “sawtooth” pattern in your memory graphs—where usage climbs steadily and then drops sharply—that’s not a leak; it’s just your GC struggling to keep up with high allocation rates. If the drops aren’t happening, you’ve got a real problem.
  • Capture a heap dump before you restart the service. I’ve learned this the hard way. If you see the memory spiking and your first instinct is to “reboot and see if it goes away,” you are effectively deleting the only evidence of the crime. Trigger a dump while the pressure is still high, or you’ll never find the culprit.
  • Check your connection pools and external client buffers. A lot of what I call “memory leaks” are actually just long-lived connections or massive response bodies sitting in a buffer because a downstream service started responding slowly. It’s not a leak in your logic; it’s a backlog in your plumbing.
  • Correlate the spike with your deployment and configuration logs. Did someone change a JVM flag or a container limit yesterday? Did a new version of a library get pulled in? Memory spikes are rarely spontaneous; they are usually the direct result of a change that looked innocent in a PR but turned out to be expensive in production.
  • Look for “the heavy hitters” in your object allocation profiles. Don’t try to analyze every single object in the heap. Use a profiler to find the top five classes by retained size. Usually, it’s something boring like a growing list of cached user sessions or a rogue logging buffer that someone forgot to cap.

The stuff I actually keep in my notes

Don’t trust your gut; trust the telemetry. If you don’t have a baseline of what “normal” looks like, you’re just guessing in the dark while the pager is screaming at you.

A memory leak isn’t a mystery; it’s just a trail of breadcrumbs. You need to find where the objects are being born and why they aren’t being allowed to die.

Documentation is your future self’s insurance policy. Write down the fix while the adrenaline is still high, or you’ll be rediscovering the same broken heap profile eighteen months from now.

The Post-Mortem Mindset

Debugging with The Post-Mortem Mindset.

At the end of the day, debugging a memory spike isn’t about finding a magic configuration flag or a new library that promises “auto-scaling.” It’s about the grunt work: getting your telemetry in order so you aren’t flying blind, distinguishing between a legitimate traffic surge and a slow, agonizing leak, and actually looking at the heap dumps instead of just staring at a red line on a Grafana dashboard. I’ve learned the hard way that if you don’t have the right observability primitives in place before the incident starts, you’re basically just guessing in the dark while the pager goes off. It’s a tedious process, but it’s the only way to move from “everything is broken” to “I know exactly which object is holding onto that three-gigabyte byte array.”

Don’t let these incidents get to you. Every time a service goes down because of a memory spike, you’re actually just being handed a very expensive, very loud lesson in how your system really behaves under pressure. I’ve spent more nights than I care to admit staring at stack traces, but those are the moments where the real engineering happens. Write down what you find, update your runbooks, and make sure the next person—whether that’s you in six months or a teammate—doesn’t have to suffer through the same 2 AM wake-up call. Build for the failure, and you’ll eventually stop fearing it.

Frequently Asked Questions

How do I tell the difference between a legitimate memory spike caused by a heavy job and an actual leak that's going to kill the node?

The simplest way to tell is to look at the baseline after the job finishes. If the memory usage returns to its previous “resting” state, it was just a heavy job doing its thing. If the graph looks like a staircase—where every spike leaves the floor a little higher than it was before—you’ve got a leak. A heavy job is a mountain; a leak is a rising tide. Watch the floor, not the peak.

If I'm looking at a heap dump, how do I actually find the specific object that's hogging everything without getting lost in a million tiny allocations?

Don’t start by scrolling through the list of every single `String` or `HashMap` instance. You’ll lose your mind. Instead, look at the “Dominator Tree.” It’s the fastest way to see which objects are actually holding onto the memory rather than just being part of the noise. Look for the big, heavy objects at the top of the tree—the ones that own a massive chunk of the heap. Follow the path to the root, and you’ll usually find the culprit.

At what point do I stop trying to tune the JVM/runtime and just admit we need more headroom or a better garbage collection strategy?

You stop when you’re spending more time tweaking `-Xmx` flags and GC tuning parameters than you are actually shipping features. If you’ve optimized your object allocation and checked for leaks, but the heap still looks like a sawtooth wave hitting the ceiling every few hours, stop fighting the runtime. At that point, it’s not a tuning problem; it’s a capacity problem. Either buy more headroom or change your collection strategy. Don’t let “optimization” become a way to avoid scaling.

How much of this is actually my code's fault versus just how the underlying library or framework handles connection pooling?

It’s usually a bit of both, but rarely a total mystery. I’ve spent enough nights staring at heap dumps to know that a library might provide the mechanism for connection pooling, but your code provides the mismanagement. If you aren’t explicitly returning connections to the pool or if your transaction logic is hanging, the library is just doing exactly what you told it to do: hold onto that memory. It’s rarely a bug in the framework; it’s usually a leak in the handoff.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.