A process with a stable workload shouldn't keep growing its resident memory. When it does, the first question isn't how much RAM is available. It's where the allocations stopped being released. On Linux, that answer isn't always obvious because the kernel, allocator, and application all influence what memory usage looks like from the outside.
Separating normal allocation behavior from an actual leak takes more than watching top or container metrics. Heap growth, allocator caches, mapped regions, and process lifetime all change the picture. A steadily increasing RSS may indicate a leak. It may also reflect fragmentation, caching, or memory the allocator hasn't returned to the kernel.
The useful evidence comes from understanding how Linux manages process memory and from using the right profiling tools to follow allocations back to their source. That's where the investigation starts.
A memory leak happens when a program asks an allocator for memory through malloc() and never releases it through free(). The allocator keeps that block marked as allocated, and the kernel has no way to know it’s functionally dead once the application loses track of it. Understanding why requires a quick look at how Linux handles virtual memory underneath malloc().
The request goes to an allocator, usually glibc’s implementation derived from ptmalloc, though latency-sensitive services often swap in jemalloc or tcmalloc instead, each using a different strategy for the same underlying problem. With glibc, small and medium allocations come from heap arenas backed by brk()/sbrk(), while large allocations are typically served through anonymous mmap() regions, with the cutoff tunable and, in modern glibc, dynamically adjusted rather than fixed. Either way, the kernel maps virtual addresses first and delays committing physical RAM until first touch, through a page fault.
From the kernel’s point of view, the process still owns that virtual address range regardless of whether anything still needs it. Whether a page stays resident, gets swapped out, or gets reclaimed follows normal virtual memory rules and current pressure, not whether the application still holds a pointer to it. With a valid pointer, the program can release the allocation through free() or munmap(). Without one, the allocation is effectively unrecoverable until the process exits.
That distinction explains a pattern every Linux administrator runs into in top or htop: the gap between VIRT and RES. VIRT is the total virtual address space a process has mapped, useful for spotting address-space growth or mmap() leaks, but not physical pressure. RES is the resident set size, the actual RAM backing that mapping. A process whose RES keeps climbing over hours or days, well past where its workload should have settled, is the most basic signature of a leak on Linux.
Leaks come from a handful of recurring patterns, and most Linux server software runs into at least one of them:
Diagnosing a leak on Linux moves through four stages: spotting the trend, ruling out look-alikes, finding the responsible code, and confirming what happened after the fact.
top or htop sorted by memory is the first signal, followed by ps aux --sort=-%mem to confirm which process is climbing. /proc/[pid]/status gives quick indicators like VmRSS and VmHWM for trend-spotting.
For mapping-level accuracy, especially when shared pages matter, use /proc/[pid]/smaps or smaps_rollup, which show memory by individual mapping and help separate a heap leak from one in a mapped file or shared library. On hosts running many copies of similar daemons, plain RSS double-counts shared pages, which is where smem earns its place, reporting proportional set size (PSS) for a more honest per-process picture.
Not every upward RSS trend is a true lost-allocation leak. Allocator fragmentation, unbounded caches, per-thread arenas, and garbage-collected heaps that don’t return memory to the OS all produce leak-like graphs.
The fix differs: a true leak needs corrected ownership and lifetime, while bloat needs cache limits, heap sizing, or allocator tuning.
Once a leak is confirmed, the next step is finding it in the code. Valgrind’s memcheck, run with --leak-check=full, classifies leaks as “definitely lost” or “possibly lost,” tied to the allocating call stack, though the overhead makes it mostly a staging tool rather than something run against live traffic. AddressSanitizer and LeakSanitizer, built into gcc and clang, are lighter and more common in CI pipelines instead, though the specific behavior depends on compiler, platform, and sanitizer configuration.
For a process already running in production, where attaching a heavily instrumented build isn’t an option, eBPF-based tools have become the standard: memleak.py from the BCC toolkit, or the bpftrace equivalent, hooks into malloc and free on a live process with much lower overhead than Valgrind, though overhead still depends on allocation rate, sampling settings, and workload. It reports outstanding allocations without requiring a restart. For JVM workloads, jmap pulls a heap dump that Eclipse Memory Analyzer or VisualVM can open to find which objects are holding the most memory and why they’re still reachable. Go ships its own answer in the standard library’s pprof heap profiler.
Kernel-space leaks are rarer, usually inside drivers rather than core subsystems. The kernel’s built-in kmemleak detector exists because user-space tools can’t see kernel memory, and slabtop offers a lighter way to watch slab cache growth without it.
When none of this happens in time, the OOM killer’s own logs become the diagnostic tool of last resort. dmesg | grep -i oom or journalctl -k usually shows which process was holding the most memory at the moment it got killed, useful retroactively even if it would have helped more a few hours earlier.
The kernel’s response to memory pressure follows a general pattern, though the exact path depends on workload, kernel settings, cgroups, and available swap. Under pressure, Linux attempts to reclaim in stages: page cache, reclaimable slab, and, depending on configuration, anonymous memory through swap. As a process’s RSS grows, the kernel typically starts with page cache, the clean, easily-recreated pages backing recently read files, reclaimed through kswapd running in the background. This is why low free memory on a Linux box often isn’t a problem: page cache is supposed to fill unused RAM and gets evicted painlessly under pressure.
The trouble starts once page cache has little left to give and the kernel has to consider reclaiming anonymous memory, the heap and stack pages behind running processes, including whatever a leak has accumulated. vm.swappiness shapes how aggressively the kernel prefers swapping anonymous memory over reclaiming file-backed cache, but it doesn’t guarantee heap and stack pages get swapped before anything is killed, since that depends on how much swap exists and how fast pressure builds. This is the stretch where a leaking service feels sluggish rather than broken, right up until reclaim can’t keep pace.
Once reclaim and swap can’t satisfy demand, the OOM killer picks a victim based on oom_score, adjustable per process through /proc/[pid]/oom_score_adj. This matters operationally: a leak in one service can get an unrelated process killed instead, if that process has a less negative oom_score_adj when the kernel goes looking for a victim. vm.overcommit_memory controls commit accounting and affects whether large allocations fail early, but it doesn’t control this later reclaim-and-OOM sequence once real pressure exists.
Inside a container, the same leak plays out differently because memory is bounded by a cgroup rather than the whole host. Cgroup v1 enforces this through memory.limit_in_bytes; cgroup v2 through memory.max. A leaking process usually hits that ceiling well before it affects the rest of the node, and gets killed by the kernel’s per-cgroup OOM handling. In Kubernetes, this surfaces as a pod entering the OOMKilled state, visible in kubectl describe pod.
The restart policy that makes containers resilient also makes leaks harder to notice. Kubernetes brings the container back up automatically, RSS resets to baseline, and the leak resumes from zero until it grows back to the limit, and the cycle repeats. Without memory metrics tracked over time, through Prometheus scraping cAdvisor or kube-state-metrics and graphed in something like Grafana, this pattern looks like an intermittent crash. It’s a deterministic leak on a timer set by request rate and the configured memory limit.
Sidecars complicate this further. Kubernetes memory limits are usually specified per container, so a leak in a logging sidecar or service mesh proxy gets killed independently of the main application, without eating into its limit directly. Newer clusters can also define pod-level resource limits, now beta and enabled by default, giving the whole pod a shared memory ceiling instead. Either way, a sidecar with no limit set can still consume from the node’s general pool, putting unrelated pods at risk of eviction.
Leaks create three kinds of security exposure: denial of service, a more specific data-exposure risk than is often assumed, and a quieter risk to the security tooling running alongside the leak.
The most direct risk is denial of service. Leak-driven DoS is a recognized attack class, formally classified as CWE-401, “Missing Release of Memory after Effective Lifetime,” not just an unfortunate side effect of sloppy code. An attacker who finds a request pattern that reliably triggers a code path missing a free() call can repeat it to drive a service toward its memory limit on purpose. The recently disclosed HTTP/2 Bomb attack is better framed as resource-amplification denial of service than a classic leak, but the result is similar: a small amount of attacker-controlled input forces disproportionate server-side resource consumption, and the outcome looks identical to an organic crash unless someone is watching for it.
A second risk is data exposure, though the mechanism is more specific than it’s often assumed to be. A true memory leak and a failure to clear sensitive data before releasing it are related but distinct problems. The practical risk with leaks specifically is duration: a buffer holding a credential, a session token, or a request body that should have lived for milliseconds instead stays resident for hours or days, simply because nothing freed it. That extended lifetime widens the window during which an unrelated bug, an out-of-bounds read, a crash that writes a core dump, a swap file persisted to disk, can expose data that a properly managed allocation would never have stuck around long enough to leak.
A third, quieter risk involves the security tooling on the same host. auditd, intrusion detection agents, and log shippers compete for the same memory as everything else, and get throttled or killed under the same pressure a leak creates, unless their oom_score_adj is specifically protected. The moment a host is under the most pressure, often because something is leaking, is also the moment its own defenses are least likely to be running normally.
Widely deployed open source daemons, Redis, Nginx, PostgreSQL, and Apache, have all had real memory leak issues tied to specific configurations or malformed input, documented in changelogs and bug trackers. Being open source means these get found and patched quickly once flagged. It also means the affected versions are public, making patch prioritization a real operational task rather than a guessing game, since anyone, including an attacker, can read the same changelog.
“Linux memory leak” sometimes refers to something happening inside the kernel itself, typically in a driver rather than a core subsystem. These leaks are harder to see because user-space diagnostic tools have no visibility into kernel memory, which is the gap kmemleak was built to fill. slabtop offers a lighter way to watch slab cache growth without its full instrumentation.
Preventing leaks comes down to ownership discipline paired with guardrails specific to the runtime in use:
The most useful signal is a trend, not a threshold. A single memory number rarely tells you anything; the slope of that number over hours and days tells you almost everything. Worth alerting on:
That last one tends to catch native extensions and off-heap leaks that a runtime’s own instrumentation never sees.
When something looks like a leak, the order of operations matters:
Anyone running into this on their own Mac, rather than a fleet of servers, can usually resolve the issue by identifying the application consuming memory and closing or restarting it before the system becomes unresponsive. In most cases, this can be done through Activity Monitor without using the Terminal or other profiling tools.
Anyone running into this on their own Mac, rather than a fleet of servers, can follow a troubleshooting guide to identify the responsible application and recover without losing unsaved work, no profiler or terminal required.
The teams that catch leaks early are usually the ones already graphing RSS over time for their long-running services, the same way they graph CPU and disk. A leak announces itself on that chart as a line that never comes back down between deploys, well before it becomes an incident. Memory tends to get treated as a fixed quantity that’s either fine or not, rather than a trend worth watching, and long-running Linux infrastructure punishes that assumption eventually, usually at the worst possible time.