Imagine you’re at a restaurant and you order a pizza (without pineapples). The waiter brings it, you eat, and when you’re done, the plate goes straight back to the kitchen. No mess, no cleanup delay, well the stack works the same way.
Now picture the same dinner, but every time you finish a plate, it stays on the table until a cleaner comes by sometime later to pick it up. That’s the heap(s). And the cleaner? That’s the Garbage Collector (GC).
The Stack: Fast and Predictable
The stack is a small but fast section of memory where local variables live while your method runs.
Each method call gets its own “stack frame” — a block of memory that holds its local variables and parameters. When the method finishes, that frame just disappears. No cleanup crew needed.
void SayHello()
{
int times = 3;
for (int i = 0; i < times; i++)
{
Console.WriteLine("Hello!");
}
}
Here, both times and i live on the stack. When SayHello() returns, the memory is gone instantly — not “eventually.” This makes your application behave in a predictable way even under heavy load.
The Heap(s)
The heap(s) is where objects/classes live. You can learn about the different heaps in a previous post I ‘ve made: Memory Management in .NET – Coding Bolt
It’s flexible and lets you store data that can outlive the method that created it, but this flexibility comes with a cost. Every time your code allocates objects that don’t fit on the stack, you add work for the GC.
It has to scan memory, trace references, and clean up objects nobody points to anymore.
If you allocate too much or too often, you’ll trigger more GC cycles. And if you’re unlucky, those cycles will happen while your API is serving real users.
That’s when your response times spike and you have to figure out the issue.
Stack Thinking in Real Code
You don’t have to abandon the heap, you just need to stop throwing objects at it like candy to a 5 year old.
Here are small ways to lean more on the stack:
1. Prefer readonly structs when possible
readonly struct Point
{
public readonly required int X { get; init; }
public readonly required int Y { get; init; }
} //notice the get, init with required. 🙂
Hot take, in my opinion, DTOs should always be readonly structs. Personally, I find no reason to mutate incoming information OR mutating a DTO you initialized to send as a response to the caller.
2. Use Span<T> or stackalloc for temporary buffers
Span<int> numbers = stackalloc int[100];
This allocates 100 integers on the stack, no GC involved. Perfect for quick calculations.
3. Avoid unnecessary allocations
String concatenations, weird LINQ chains, boxing, closures — all of these quietly fill the heap.
A Karate Kid lesson
As Mr Miyagi says, you need to find the balance.
The stack has limits, literally. Trying to use it with huge amounts of data and you’ll get a StackOverflowException.
The point of this whole post is to shift your way of thinking and start using readonly structs and best practices to avoid heap allocations, not completely removing classes from your code.
Affiliate promo
If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!
Leave a comment