My Server Crashed Every Night: Understanding Stack vs Heap
1. The Mystery of the Cinderella Server
I had a strange issue with a Node.js server I was maintaining last year.
It ran perfectly fine during the day, but restarted every night at 12 AM. (Like Cinderella).
The logs revealed a horrific message right before death:
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
"Out of memory? But the AWS instance has 16GB of RAM!"
It turned out the RAM usage from the OS perspective was fine, but the V8 Heap limit was reached.
Node.js (before v12) had a default heap limit of about 1.4GB. My app was hitting that ceiling.
This incident forced me to open the black box called "Memory" and understand what was happening inside.
2. Two Faces of Memory: Tidy Desk vs Messy Warehouse
Where do variables go when we write code?
Mainly into two places: Stack and Heap.
Here is the easiest analogy for non-CS majors.
Stack = "Sticky Notes on My Desk"
- Traits: Extremely fast, small size, highly organized (LIFO - Last In, First Out).
- Usage: Tasks to handle right now (Function execution context, local variables).
- Lifespan: Crumpled and thrown away immediately after the task (function) ends.
- Contents: Lightweight primitives like
int, boolean, double.
- Analogy: You stick a note for "Task A". When Task A calls Task B, you stick note B on top of A. When B is done, you peel it off. Fast and clean.
Heap = "Massive Logistics Warehouse"
- Traits: Huge, slower access, disorganized (Fragmentation occurs).
- Usage: Large items of unknown size at compile time (Objects, Arrays, Class Instances, Closures).
- Lifespan: Stays forever until someone specifically says "Throw this out." (Manual
free() in C/C++, Garbage Collector in Java/JS/Python).
- Contents: Heavy reference types like
Object, Array, Map.
- Analogy: "Store this user data." You put a big box in the warehouse. On your desk (Stack), you only keep a slip of paper describing where the box is (Address/Pointer).
3. Root Cause Analysis: The Warehouse Exploded
My server died not because of the Stack, but because of the Heap.
Stack memory is automatically reclaimed when functions return. But Heap memory depends on your logic.
Looking at the code, the culprit was trivially simple yet devastating.
// The Suspect
const requestLogs = []; // Global Variable (Stored in Heap)
app.use((req, res, next) => {
// Pushing data on every request
requestLogs.push({
url: req.url,
time: Date.now(),
headers: req.headers // Headers can be surprisingly large
});
next();
});
I thought, "Let's keep some logs in memory for debugging." That global array requestLogs was the problem.
This array lives in the Heap.
Every request added data to it, but no one ever emptied it.
- Stack variables (
req, res) disappear after the response is sent.
- Heap variables (
requestLogs) survive as long as the process lives.
- As traffic grew, the array grew to millions of items.
- Eventually, the warehouse got full (hit the 1.4GB Heap Limit), and the V8 engine crashed.
4. Deep Dive: What is a Reference?
This brings us to the crucial concept of "Reference".
The Stack only holds the address (sticky note), while the actual data sits in the Heap (warehouse).
let user = { name: "Ratia" };
- Heap: Allocates memory for
{ name: "Ratia" }. (Address: 0x1234)
- Stack: Writes
0x1234 on the variable user.
What if we do user = null?
The sticky note on the Stack is wiped.
But { name: "Ratia" } inside the Heap remains.
This is when the janitor, Garbage Collector (GC), comes in. It scans the warehouse.
"Hey, does anyone have the receipt for this box (0x1234)?"
"No? Okay, it's trash. Burn it." (Deallocation).
In my failed code, the requestLogs global variable kept holding references to the log objects.
So the GC thought, "Ah, this data is still needed because requestLogs refers to it," and refused to clean it.
This is a Memory Leak. You are hoarding trash because you forgot to tell the janitor it's trash.
5. Visualizing the Crash
Imagine a library.
- Stack is the librarian's desk. It's small but efficient. Books come and go instantly.
- Heap is the bookshelves. They are vast.
My code was effectively doing this:
Every time a user visited, I took a book, wrote a log in it, and put it on a shelf. But I never threw old books away.
Eventually, the shelves were full. The librarian (V8 Engine) tried to make space (Mark-and-Sweep GC), but every book was marked as "Important" because the global array referenced them.
The librarian panicked ("Ineffective mark-compacts") and finally collapsed ("Fatality").
6. The Fix: Clean the Warehouse
The solution was simple. Don't use application memory (Heap) for logs. Use disk (File) or external storage (Redis/Elasticsearch).
// Solution: Write to file using Stream (Don't use Heap)
const fs = require('fs');
const path = require('path');
// Create a write stream
const logStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' });
app.use((req, res, next) => {
// Writes directly to file buffer, minimal memory footprint
logStream.write(`${new Date().toISOString()} ${req.url}\n`);
next();
});
After this change, the Heap memory usage flattened out, and the server never crashed again.
If you must use in-memory storage, always limit the size (e.g., Circular Buffer).
7. Extended Knowledge: What about Stack Overflow?
While my issue was Heap Overflow, what is Stack Overflow?
It happens when you pile too many sticky notes on your desk.
function recurse() {
recurse(); // Calls itself forever
}
recurse();
Each function call adds a Stack Frame. Since the Stack size is fixed per thread (usually around 1MB), infinite recursion fills it up instantly.
Unlike Heap issues which are slow and insidious (leak over days), Stack issues are immediate and usually caused by logical bugs in recursion.
8. Conclusion: Memory is Not Free
With modern languages like JS, Python, or Go, you can develop without manually managing memory (malloc/free).
But "You don't have to manage it" does NOT mean "You don't have to worry about it."
- Global Variables are dangerous. Use them sparingly.
- Closures can inadvertently hold onto large scopes. Be careful.
- Streams are your friend for large data. Don't load a 1GB file into a variable; stream it.
There might be a monster (Memory Leak) growing in your server right now, waiting for midnight.
Check your monitoring dashboard today.