On this page
  1. What is a memory leak?
  2. What causes memory leaks?
  3. How do I find a memory leak?
  4. How do I fix a memory leak?
  5. Where AI coding assistants get memory leaks wrong
  6. Checklist
  7. FAQ
    1. What is a memory leak in simple terms?
    2. Does Python have memory leaks if it has garbage collection?
    3. Why does my app get slower over time and then crash?
    4. How do I find a memory leak?
    5. Is a memory leak always a programming error?
  8. Related topics
  9. Sources
concept

What Is a Memory Leak (and Why Does Your App Get Slower the Longer It Runs)?

A memory leak is memory your app never releases. Learn why AI-generated code leaks it, how to find the cause, and how to fix it in Python and Node.js.

Quick answer

  • A memory leak is memory your program allocated, stopped needing, and cannot release — so it stays reserved and unavailable.
  • The signature is gradual growth, sudden failure: response times creep up, then the process is killed when it hits its limit.
  • Garbage collection does not help when your own data structures still hold a reference to the “released” data.
  • Find it by measuring across a long run, then diffing two heap snapshots; the objects that grew are the leak.

What is a memory leak?

A memory leak is memory that a program has allocated, no longer needs, and cannot give back to the operating system. The memory is not corrupted or lost — it is still owned by your process, still reserved, and still counted against your limit, so it is unavailable to handle the next request.

Because the operating system hands out finite memory to a process, a leak is always a countdown. The process grows, and the failure arrives abruptly when a limit is crossed: a Python MemoryError, a Node.js JavaScript heap out of memory crash, or a container killed with a non-zero exit code. In containers that is the OOMKilled condition covered in Why Do My Containers Keep Getting Killed (OOMKilled)?, and the underlying capacity story is in What Is RAM (and Why Does Your App Run Out of Memory)?.

Where this bites vibecoders

The specific way this bites vibecoders is the module-level cache. An assistant asked to “avoid recomputing this expensive lookup” will add a cache = {} at the top of the file, or a _instance variable on a class, or an in-memory list that accumulates results. That is a sensible optimisation in a script and a leak in a server, because the process never restarts. Nothing in the code looks wrong; the code is simply not written for a process that runs for months.

What causes memory leaks?

A leak almost always comes down to one of three things: a reference that outlives the data, a container that only ever grows, or a resource that was opened and never closed.

References that outlive their data. An event listener attached to a long-lived object, a timer that is never cleared, a closure capturing a scope it should not, a closure registered in a module-level array. If object A holds object B and B holds data, and nothing ever releases A, everything below it stays alive too.

Containers that only grow. In-memory caches, arrays used as logs, Sets used for deduplication, a map from user ID to session. These are correct designs right up until the population is bounded in your head and unbounded in reality.

Unclosed resources. File handles, database connections, sockets, subprocess pipes. These hold memory and file descriptors, so the symptom is often the connection-pool error described in What Is a Connection Pool?.

A fourth case deserves separate mention because it is not technically a leak: a fragmented heap. Memory is all still reachable and all still needed, but the allocator cannot serve new requests from it. It presents identically to a leak and is fixed differently.

How do I find a memory leak?

Prove it before you hunt it. A single request tells you nothing; the leak is a function of volume and time.

1. Establish a baseline. Run your app under a steady load for several minutes and sample resident memory. Use time on a CLI run, docker stats for a container, or your platform’s metrics. Memory that is flat is healthy; memory that rises steadily after warm-up is leaking.

2. Take two heap snapshots, far apart. The gap is what matters. Anything that grew between them without a matching reason is the leak.

# Python: attribute growth to a source line
python -X tracemalloc=25 -m your_app
# then inspect with tracemalloc.take_snapshot() at two points

# Node.js: capture heap snapshots without restarting
node --inspect your_app.js
# in DevTools: Memory → take heap snapshot, wait, take another, then compare

3. Read the growth report. In Python, tracemalloc prints the top lines by allocated size, which usually points straight at the accumulating list. In Node.js, the comparison view groups retained objects by constructor, and the class that grew — Map, Array, or a class of yours — tells you which structure to inspect.

4. Shrink the suspect until the leak disappears. If the growth is a cache, give it a size bound or a TTL and re-measure. If the app is stable, the diagnosis was right. This is faster and more certain than reading code for leaks, which rarely works.

How do I fix a memory leak?

Fix the cause rather than raising the limit. Raising the limit converts a slow failure into a slow catastrophe.

CauseFix
Unbounded cache or listBound it: max entries, or a TTL that evicts stale entries
Module-level state across requestsMove it per-request, or clear it explicitly
Event listener never removedStore the reference and remove it on teardown, or use a weak reference where semantics allow
Timer never clearedClear it in the same place you created it, and on shutdown
Database connection or socket not closedUse a context manager or try/finally; pool rather than open per request
Subprocess pipe not drainedRead the pipe fully, or close the handle; see What Is Graceful Shutdown?
Genuine fragmentationRestart on a schedule, or move hot allocation patterns to a preallocated buffer

The recurring theme is lifecycle ownership: whoever creates a resource is responsible for releasing it, and for a long-running server that responsibility has to be written down, because nothing forces it.

Where AI coding assistants get memory leaks wrong

  • Module-level mutable state that grows. const seen = new Set() at file scope, added to on every request. This is the most common generated leak by a wide margin.
  • Caching without eviction. The assistant adds a cache to avoid repeated work and implements none of the part that makes a cache safe under load: bounds, expiry, or invalidation.
  • Event listeners added in a loop or on every render. In a long-running process the handler count grows without limit.
  • setInterval with no clearInterval. Especially in polling code, which is otherwise reasonable to generate.
  • Database clients created per request instead of a single pooled client, so connections and their buffers accumulate.
  • Reading a whole large file or response into memory to parse it, where streaming is the natural shape.
  • No cleanup path at all. Generated code has no shutdown handler, so nothing is released on deploy or restart.

Checklist

  • Measure memory across a sustained run, not a single request, and confirm the growth is monotonic.
  • Take two heap snapshots and identify the object type that grew.
  • Bound every cache by size or time, and evict.
  • Audit module-level variables for anything that accumulates across requests.
  • Pair every addEventListener with a matching removal, and every setInterval with a clearInterval.
  • Close files, sockets, and connections with context managers or try/finally.
  • Reuse one database client per process through a pool instead of connecting per request.
  • Stream large files and responses instead of reading them whole.
  • Add a shutdown path that releases resources, and verify it runs on deploy.
  • Only raise the memory limit after the cause is fixed and measured.

FAQ

What is a memory leak in simple terms?

A memory leak is memory your program allocated, no longer needs, and cannot release. The memory stays reserved and unavailable for anything else, so the process’s resident size grows over time until the operating system or your container runtime kills it, usually with an out-of-memory error.

Does Python have memory leaks if it has garbage collection?

Yes. Garbage collection reclaims objects nothing references, but it cannot help when your data structures still hold a reference. A list that accumulates every request object, a module-level dict that is never cleared, or a closure that captures its enclosing scope all keep memory alive and are all common in generated code.

Why does my app get slower over time and then crash?

Slower-then-crash is the classic leak signature. As memory fills, the allocator works harder and garbage collection runs more often, so response times climb. Once the limit is hit, the process is killed abruptly. The growth is gradual and the failure is sudden, which is why it often reaches production before anyone connects the two.

How do I find a memory leak?

First prove it is real by watching resident memory across a long run or repeated load test, not a single request. Then take two heap snapshots several minutes apart and diff them: the objects that grew are the leak. In Python, tracemalloc attributes growth to a specific line; in Node.js, --inspect plus heap snapshots does the same.

Is a memory leak always a programming error?

Not always. A long-lived cache that grows to match real traffic can be intentional, and what looks like a leak may be fragmentation where memory is all still needed but cannot be reused. Both present the same way, which is why measuring and diffing heap snapshots comes before changing any code.

Sources

Share: