Memory Management and Resource Exhaustion

Problem

Your service starts quickly, then becomes sluggish after several hours. Memory usage keeps growing, although CPU usage is modest. Restarting helps temporarily, even though the application uses a language with automatic memory management.

Solution — Memory and Resource Management

1. Stack: keep track of active function calls

The call stack records which functions are currently active and where execution should return. Each ordinary function call adds a stack frame: a small working area containing return information and some temporary values, such as local variables or arguments. Compilers may keep some values in CPU registers. If HandleRequest calls BuildProfile, BuildProfile is the most recent call. When it returns, its frame is removed and HandleRequest continues. When HandleRequest returns, its frame is removed too. The space from those frames can be reused. This order is called last in, first out.
HandleRequest starts → add its frame
BuildProfile starts  → add a frame above it
BuildProfile returns → remove its frame
HandleRequest returns → remove its frame
In a typical threaded runtime, each thread has its own call stack. These are ordinary synchronous calls; runtimes can preserve suspended async methods' state separately.

2. Heap: keep data with a separate lifetime

The heap provides memory for allocated data, such as objects that can remain useful after the function that created them returns. Their lifetime is managed separately from that function's stack frame. In a garbage-collected runtime such as .NET, an object that is still reachable through references is kept available. Once it becomes unreachable, it can be reclaimed by a later garbage collection. Returning from a function does not by itself delete every object that function created. A reference and its object are different things. A function can hold a local reference that lets it access an object on the heap. Returning that reference lets the caller access the same object; the object itself does not need to move from the stack to the heap at that point. The exact placement of values depends on the language, runtime, and compiler optimizations. Use call lifetime versus object lifetime to understand the difference.

3. Example: a profile survives the function that creates it

Consider an ordinary C#/.NET method named BuildProfile that creates an instance of a Profile class and returns it to HandleRequest. This is a simplified view of the allocations. Step 1 — HandleRequest calls BuildProfile. Both calls are active on the stack. Step 2 — BuildProfile creates the Profile object on the managed heap. Its local reference provides access to that object. Step 3 — BuildProfile returns the reference. Its stack frame is released, while HandleRequest can still use the Profile object through the returned reference. Step 4 — HandleRequest stores the reference in an application-wide cache and returns. Its own frame is released. The cache still makes the Profile object reachable, so the object remains available.
Cache → reference → Profile object on the heap
If the cache entry is removed and no other reachable reference keeps the object alive, the object becomes eligible for garbage collection. Collection can happen later; removal from the cache does not promise immediate cleanup.

4. How either kind of memory can run out

Deeply nested calls, such as uncontrolled recursion where a function keeps calling itself, can exhaust the call stack. This is a stack overflow. A cache that keeps adding heap objects can cause growing memory use. The garbage collector cannot know that you have finished with objects the cache still references. This is one way a memory leak can persist despite automatic collection. Under memory pressure, some systems move memory pages between RAM and disk. Heavy paging can make requests slow; hitting a memory limit can cause allocations to fail or the process to be terminated. Memory is only one finite resource. Requests also wait when all database connections or worker threads are occupied. Measure memory growth, collection activity, paging, and pool usage. Bound caches, release resources when finished, and limit concurrent work.
Core idea: stack frames follow active function calls; heap objects have separately managed lifetimes. Keep ownership clear, bound growth, and release resources when finished.