Memory Leak

Concept

Memory that a program is finished with but never hands back, piling up until the software gets slower the longer it runs and eventually stops.

Why it exists

Programs borrow memory whenever they handle data and return it when they're done. Modern languages carry a collector that finds data nothing uses anymore and clears it automatically, so nobody has to write the return by hand. The catch is the test the collector applies. It isn't "nothing uses this," it's "nothing points at this." Data everyone has forgotten about still survives as long as one finger somewhere is resting on it.

The gap between those two tests stayed invisible on the older web. Every link click fetched a whole new page, so everything accumulated got wiped each time the page changed. With a full cleanup every few minutes, a small leak never had time to add up to anything a user would notice.

That changed once the single-page application (SPA) approach spread, where one document stays loaded and only the view gets swapped. Open the tab once and it survives twenty navigations. Pagedeck got exactly this support ticket: the document editor gets sluggish in the afternoon. Theo opened it in the morning, clicked around, and found nothing wrong. It only happened in the browser of someone who opened the tab at nine and left it open.

How it works

Data the program has stopped using is still being pointed at, so the collector can't clear it, and the leftovers accumulate as that situation repeats.

The pointers usually survive through one of four paths.

  1. Event handlers that never get detached. A function attached to window resizing or scrolling doesn't fall off by itself when the view closes. If it still holds data from that closed view, every open and close leaves another chunk behind.
  2. Timers that never get stopped. A check scheduled to run every three seconds keeps running after the user navigates away. Add one per editor session and by afternoon the same check fires several times a second.
  3. A global store that grows without limit. Caching a document so it doesn't have to be fetched twice is a performance technique, but with no rule for taking things out, the store grows for the entire session. A size cap and an eviction rule are what make it a cache; without them it's a leak.
  4. Variables holding on to elements that are gone. Push a deleted list item into an array in case it's needed later and it disappears from the screen while staying in memory, along with everything it references.

The shape of the growth is a staircase. Each individual leak is small enough that a few minutes of use shows nothing, and it climbs a little with every navigation. Past a certain point the collector starts running more often and for longer, and that's when users notice input landing a beat late. At the far end the tab dies outright, usually taking unsaved work with it.

Where it shows up

The term comes up most in products where people never close the tab — collaborative document tools, chat, monitoring dashboards. Metrics that measure the opening moment, like largest contentful paint (LCP), finish measuring before any of this happens and can't detect it at all. Interaction to next paint (INP), which times how long the screen takes to respond to a tap or a keystroke, does degrade in a long-lived tab, but by then the user has already felt it.

Servers get the same label. A process that stays up for weeks and skips a small return on each request will exhaust its memory and be killed a few days later. It restarts automatically, so users see a brief error and move on, but without a root cause the same incident repeats every few days at an hour nobody chose. Teams that deploy often find it latest, since shipping a new version every other day restarts the process and erases the symptom before anyone connects the dots.

Finding one means looking at time rather than at a moment. In a tool that tracks memory usage, such as application performance monitoring (APM), a graph that climbs steadily instead of sawing up and down is the signal. In a browser, repeat the suspect action about twenty times and take a heap snapshot before and after. If twenty objects of the same type have appeared, that's the leak. How much observability a team has built — how much of the inside is visible from outside — largely decides how long the search takes.

How it sounds at work

"This looks like a memory leak. Reproducing it probably means leaving the tab open for three hours."

Warning in advance that a few minutes of clicking around won't surface it, so the bug report needs a different kind of test.

"I opened and closed the editor twenty times, took a heap snapshot, and there are twenty listeners still sitting there."

Reporting that the leaking spot has actually been pinned down.

"Let's schedule an overnight restart for now and look at the cause separately."

Covering the symptom temporarily while keeping the real investigation on the list.

At Pagedeck the problem only got caught after about ten afternoon-sluggishness tickets had piled up. The first few were filed as user hardware issues. Mia noticed the timestamps clustered entirely in the afternoon, which no hardware explanation accounts for, and handed them to Theo, who opened and closed the editor twenty times while watching memory. It was one line that failed to detach a scroll handler when the view closed.

What people get wrong

The most common view is that a restart fixes it, so it isn't serious. That conclusion is easy to reach because one line of configuration scheduling an overnight restart really does make the symptom disappear. But a restart empties what has accumulated without changing the rate of the leak. When usage doubles and so does the rate, the process that lasted a day dies in half a day, and the service now drops at lunchtime instead of at 4 a.m. The longer the symptom stays covered, the deeper the responsible code gets buried under everything written since.

The second misconception is that a language with automatic memory management can't leak. It's true that returns don't have to be written by hand, but as above, automatic collection only happens when nothing points at the data. One undetached handler or one unstopped timer keeps breaking that condition. In languages where returns are written explicitly, a missing one at least appears in the code; where the collector handles it, the mistake shows up nowhere, which makes it harder to find rather than easier.

Where to go next

  • Web performance

    see where this problem sits among the metrics and the techniques

  • LCP

    compare it with a metric that only measures the first screen and therefore misses this entirely

Last updated 2026-09-16