← Open in the full interactive course (progress tracking, search & more)

The one allocation the garbage collector will never, ever see — because it isn't on the heap at all.

Back in Advanced Part I, the stack-vs-heap lesson mentioned stackalloc in a single forward-looking paragraph and then deliberately moved on, promising "a dedicated Advanced module later." This is that module. You now have everything you need to understand it properly: precisely how the stack reclaims memory (last lesson's whole topic), and exactly what a ref struct is and why it's the safe wrapper around what stackalloc produces (the previous lesson).

stackalloc is the one allocation mechanism in all of C# that has genuinely nothing to do with the garbage collector — not "rarely collected," not "usually cheap to collect" — never tracked by the GC at all, because it never touches the managed heap in the first place.

In this lesson, you'll learn exactly what stackalloc allocates and where, the modern span-wrapped syntax that makes it safe to use in ordinary code, genuine real-world use cases for it, and the one real danger — stack overflow — that makes it a tool you reach for deliberately, not by default.

What Is It?

The Simple Explanation

stackalloc allocates a block of memory directly on the current method's own stack frame — the exact same region of memory that already holds your local ints and decimals, per the mental model from two lessons ago. It is reclaimed the instant the method returns, automatically, with zero GC involvement — exactly like every other local value-type variable.

The Technical Definition

stackalloc is a C# operator that reserves a contiguous block of uninitialized (or, for value types, zero-initialized by default) memory on the calling method's stack frame, and produces either a pointer to that memory (the original, unsafe-only syntax) or a Span<T>/ReadOnlySpan<T> wrapping it (the modern, safe syntax introduced in C# 7.3). Because that memory is not tracked by the garbage collector at all — it isn't part of any generation, it has no object header, it's never scanned during a collection — using stackalloc produces genuinely zero heap allocation for that buffer.

Modern — Span-wrapped (idiomatic)

Legacy — raw pointer (unsafe)

Why Does It Exist?

The Problem

Some hot-path code needs a small, short-lived scratch buffer — a handful of bytes to build a hash, a few dozen characters to format a number, a temporary staging area while parsing. The obvious tool is new byte[64], but that's a heap allocation: it goes through the allocator, gets an object header, gets tracked by Gen0, and eventually has to be traced and collected — real, if individually small, work for the GC, as the Generational GC lesson in Advanced Part I covered in detail. In a method called millions of times per second, thousands of these tiny, disposable arrays add up to real, measurable GC pressure, purely for memory that's used for microseconds and then thrown away.

The Solution

If a buffer's entire lifetime fits neatly within one method call — allocated at the top, used and discarded before the method returns — it has exactly the lifetime profile the stack already handles for free, as the previous stack-vs-heap lesson established. stackalloc lets you deliberately put that buffer there instead of the heap: reclamation is automatic and instantaneous on return, with zero tracing, zero collection, and zero GC pressure whatsoever — the fastest possible way to get temporary memory in .NET, precisely because it sidesteps the garbage collector entirely rather than merely being cheap for it to collect.

Big Picture

new byte[64] VS stackalloc byte[64]
new byte[64] — HEAP ARRAY
vs
stackalloc byte[64] — STACK BUFFER

How It Works

The modern, idiomatic syntax

Span<byte> buffer = stackalloc byte[256];

buffer[0] = 0xFF;          // bounds-checked, safe indexing
buffer.Fill(0);            // ordinary Span<T> API, no unsafe code
var slice = buffer[..16];  // slicing works exactly as it does for any Span<T>

Notice that buffer is typed as Span<byte> — a ref struct, per the previous lesson. That's not incidental: it's the exact mechanism that makes this safe. Because Span<T> can never be boxed, stored in a field, captured by a closure, or held across an await, the compiler guarantees this stack-allocated buffer can never be smuggled somewhere it would outlive the stack frame it lives on. The ref struct restrictions from the last lesson exist largely because of exactly this scenario.

The legacy, unsafe syntax — for context, not for regular use

unsafe
{
    byte* buffer = stackalloc byte[256];
    buffer[0] = 0xFF; // no bounds checking — buffer[9999] compiles and corrupts memory silently
}

This is the original stackalloc syntax, predating Span<T> (which arrived in C# 7.2/7.3). It requires an unsafe context, gives you a raw pointer with no bounds checking at all, and you'll still encounter it in older codebases and in genuinely low-level unsafe interop code. For virtually all new code, prefer the Span<T>-wrapped form — you get identical stack-allocation behavior with real safety on top, at no performance cost.

Simple Example — formatting a number without allocating a string

public static void WriteFormattedId(int id, TextWriter writer)
{
    Span<char> digits = stackalloc char[10]; // int.MaxValue has 10 digits, max

    if (id.TryFormat(digits, out int written))
    {
        writer.Write(digits[..written]);
    }
}

Why this avoids allocation entirely: int.TryFormat writes the formatted digits directly into the span-wrapped stack buffer — no intermediate string is ever allocated on the heap just to hold the digits before writing them out. Compare this to the far more common writer.Write(id.ToString()), which allocates a new heap string on every single call. For a method called millions of times, that's a real, measurable difference in GC pressure for a code path that gains nothing lasting from the allocation — the string exists for microseconds and is immediately discarded.

Real-World Example — a bounded, safe hashing buffer with an ArrayPool fallback

Real production code almost never uses a fixed-size stackalloc blindly — it uses a small, safe threshold, and falls back to ArrayPool<T> (covered elsewhere in this Part) for anything larger:

private const int StackAllocThreshold = 256;

public static string ComputeHash(ReadOnlySpan<byte> data)
{
    Span<byte> hashBuffer = stackalloc byte[32]; // SHA-256 output is always exactly 32 bytes — safe, fixed

    if (data.Length <= StackAllocThreshold)
    {
        Span<byte> scratch = stackalloc byte[StackAllocThreshold];
        data.CopyTo(scratch);
        SHA256.HashData(scratch[..data.Length], hashBuffer);
    }
    else
    {
        // Too large for a safe stack buffer — rent from the pool instead
        byte[] rented = ArrayPool<byte>.Shared.Rent(data.Length);
        try
        {
            data.CopyTo(rented);
            SHA256.HashData(rented.AsSpan(0, data.Length), hashBuffer);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(rented);
        }
    }

    return Convert.ToHexString(hashBuffer);
}

Notice the pattern: the stackalloc size is a small, fixed, compile-time-known constant (StackAllocThreshold, 256 bytes) — never sized directly by data.Length, which could be arbitrarily large if it ultimately traces back to user input. Anything above the threshold is routed to ArrayPool<T> instead, which can safely handle any size without risking the stack. This threshold-plus-fallback shape is the standard, idiomatic way real .NET code (including parts of the BCL itself) uses stackalloc safely.

Analogy

A tape measure you already have in your pocket

Reaching for new byte[64] is like walking to a hardware store every time you need to measure something — reliable, but there's real overhead in the trip, even for a five-second job. stackalloc is like already having a small tape measure in your pocket: instant, free, no trip required — but your pocket only has so much room. Try to carry a stepladder in there (an oversized or unbounded stackalloc) and something breaks. That's exactly why you use the pocket tape measure for small, quick, genuinely bounded jobs, and go to the hardware store — ArrayPool<T> — for anything bigger.

Under the Hood — the real danger: stack overflow

WHY STACKALLOC SIZE DISCIPLINE ISN'T OPTIONAL
1. STACK SPACE IS SMALL AND FIXED, PER THREAD
2. TWO WAYS TO EXHAUST IT WITH stackalloc
3. THE RESULT: StackOverflowException — AND IT CANNOT BE CAUGHT
4. WHAT THIS MEANS FOR HOW YOU USE stackalloc

Common Confusion

1. "stackalloc always requires unsafe code"

Only the legacy raw-pointer form does. The modern Span<byte> buffer = stackalloc byte[256]; form requires no unsafe keyword whatsoever and has been the idiomatic way to write this since C# 7.3 — the safety comes from the ref struct guarantees of Span<T>, covered in the previous lesson.

2. "A StackOverflowException is like any other exception — just wrap it in try/catch"

This is the single most important correction in this lesson: it cannot be caught. Ever. By anything. A stack overflow terminates the process on the spot. This is fundamentally different from every other exception you've worked with across this entire course, and it's precisely why the size discipline in this lesson matters so much more than it would for an ordinary recoverable error.

3. "stackalloc memory is garbage collected, just very quickly"

No — it isn't garbage collected at all, at any speed. "Very fast collection" and "no collection involved whatsoever" sound similar but aren't the same thing. stackalloc memory is never part of any GC generation, is never traced, and is never subject to a collection pause. It's reclaimed purely by the stack pointer moving back when the method returns — the exact same zero-GC mechanism as any other local variable, as the previous lesson established.

Common Mistakes

Mistake 1 — Sizing a stackalloc directly from user or request input

Span<byte> buffer = stackalloc byte[userSuppliedLength]; — if userSuppliedLength comes from anywhere an attacker or careless caller could influence, this is a genuine, process-crashing denial-of-service vector, not just a bug.

Clamp against a small, safe constant threshold, and route anything larger to ArrayPool<T> — never trust an external value as a stack allocation size directly.

Mistake 2 — Calling stackalloc inside a loop, growing the live stack footprint each iteration

Placing a stackalloc inside a for loop body when the buffer isn't actually needed fresh each iteration, or inside recursive calls without a hard depth limit — each nested call's buffer stacks on top of the last, un-reclaimed until that specific call returns.

Hoist a single stackalloc above the loop when one reusable buffer will do, and always cap recursion depth explicitly when a recursive method also uses stackalloc.

Mistake 3 — Reaching for stackalloc as a default "faster than new[]" habit

Replacing every small array allocation in a codebase with stackalloc on the assumption that it's simply, unconditionally better.

Reserve it for genuinely hot, measured paths with small, provably bounded sizes — for ordinary code, a small heap array is simple, safe by construction, and the GC handles Gen0 garbage cheaply anyway (per the Generational GC lesson). The next two lessons in this Part cover exactly how to know whether a path is hot enough to deserve this treatment in the first place.

When Should I Use It?

Use stackalloc when

Avoid stackalloc when

Rule of thumb: If you can't confidently state the exact maximum size, in bytes, that a stackalloc could ever request at that call site — don't use it, or add an explicit check that falls back to ArrayPool<T> above a safe threshold. "Small and provably bounded" is not optional.

Mental Model

stackalloc = a local value-type variable, just bigger — same stack frame, same automatic reclamation on return, zero GC involvement.

Small and bounded, always. The stack is roughly 1 MB per thread, shared with every other local and every nested call frame.

The one failure mode that can't be caught: a real StackOverflowException kills the process instantly — try/catch cannot save you. This is exactly why size discipline here is non-negotiable, not merely good practice.

Key Takeaway


Check Your Understanding

You now understand exactly where stackalloc memory lives, how to use it safely, and precisely what can go wrong. Let's confirm it.

1. Why is memory allocated with stackalloc never subject to garbage collection?

Show answer

Correct: B

Why B is correct: The GC only manages the heap. stackalloc memory lives on the stack — an entirely separate region — so it was never eligible for GC tracking in the first place, not merely fast to collect.

Why A is incorrect: There's no such special GC generation — the stack isn't part of the generational GC system at all.

Why C is incorrect: "Rooted" is a heap-object GC concept describing reachability; it doesn't apply to stack memory, which was never a GC-managed object.

Why D is incorrect: Pinning is a mechanism for preventing the GC from moving a heap object during compaction — irrelevant to stack memory, which the GC never touches regardless.

Reinforcement: "Not GC-tracked" and "quickly GC-collected" are genuinely different things — stackalloc memory is the former, not the latter.

2. What is the modern, idiomatic way to write a stackalloc expression in current C#, and why is it preferred?

Show answer

Correct: B

Why B is correct: Wrapping the stackalloc'd memory in a Span<byte> requires no unsafe context and gives you bounds-checked, safe indexing — this has been the idiomatic form since C# 7.3, and is what the previous lesson's ref struct restrictions specifically exist to make safe.

Why A is incorrect: This is the legacy, pre-Span<T> syntax — it still works but requires unsafe and offers no bounds checking; it's not the recommended modern form.

Why C is incorrect: This wouldn't compile — a ref struct like Span<byte> can never be boxed to object, and even the raw pointer form can't be assigned to object directly.

Why D is incorrect: stackalloc doesn't produce a List<T>-compatible value, and wrapping stack memory in a resizable heap collection would defeat the entire point of stack allocation.

Reinforcement: The Span<T>-wrapped form is both safer and no slower than the raw pointer form — there's essentially no reason to prefer the legacy syntax in new code.

3. A method calls stackalloc to reserve memory sized directly from a value read off an incoming network request, with no upper bound check. What is the most serious risk?

Show answer

Correct: B

Why B is correct: This is exactly the danger the lesson calls out explicitly: an unbounded, externally-influenced stackalloc size is a genuine process-crashing risk — and because a real stack overflow cannot be caught by any try/catch, there's no way to recover from it once it happens.

Why A is incorrect: stackalloc memory is never subject to GC in the first place, so there's no "future collection" that applies here — and the actual risk is far more severe than a leak.

Why C is incorrect: stackalloc size can be a runtime-computed expression, not only a compile-time constant — that flexibility is exactly what makes the unchecked-input scenario dangerous.

Why D is incorrect: This significantly understates the risk — an uncaught stack overflow is a full process crash, not a minor performance issue.

Reinforcement: Never size a stackalloc from unbounded external input — always clamp against a small, safe threshold and fall back to ArrayPool<T> above it.

4. Why is a StackOverflowException fundamentally different from almost every other exception type in .NET?

Show answer

Correct: B

Why B is correct: This is the single most important, distinctive fact about a real stack overflow in .NET — by the time it's detected, the runtime can't trust it has enough remaining stack space to safely run exception-handling machinery, so it terminates the process outright rather than attempting recovery.

Why A is incorrect: Stack overflow can occur in any method — synchronous or asynchronous — that exhausts its thread's stack, typically via deep or runaway recursion.

Why C is incorrect: There's no automatic retry mechanism — the process terminates immediately upon detection.

Why D is incorrect: Stack overflow is a real runtime condition tied to actual stack exhaustion — it occurs in Release builds exactly as it does in Debug builds, whenever the stack genuinely runs out.

Reinforcement: This uncatchable, process-ending behavior is precisely why size discipline around stackalloc (and deep recursion generally) is a hard requirement, not just good style.

5. A developer needs a buffer to compute a hash of data whose size could range from a few bytes up to several megabytes, depending on the caller. What's the correct approach based on this lesson?

Show answer

Correct: B

Why B is correct: This is exactly the threshold-plus-fallback pattern shown in the Real-World Example — stackalloc handles the common, small case cheaply and safely, while ArrayPool<T> safely absorbs anything above the safe stack threshold, regardless of how large the input gets.

Why A is incorrect: Sizing stackalloc directly from a potentially multi-megabyte input is precisely the dangerous, unbounded pattern this lesson warns against — it risks a process-terminating stack overflow.

Why C is incorrect: stackalloc remains an excellent choice for the small, common case even when the overall input size varies — the fix is bounding it with a threshold, not avoiding it entirely.

Why D is incorrect: Calling stackalloc repeatedly inside a loop, especially for large data, risks accumulating stack usage across iterations depending on how the loop is structured, and adds unnecessary complexity compared to the simple threshold-and-fallback approach.

Reinforcement: The threshold-and-fallback pattern is the standard, production-proven way to get stackalloc's speed for the common case without its risk for the uncommon one.

You've delivered on the promise from Advanced Part I — you now know exactly what stackalloc allocates, how to use it safely, and precisely why its one real danger demands real discipline. Next, you'll pull every tool from this Part together into one coherent, zero-allocation programming style.


dotnetmadeeasy.com — Learn C# and .NET, the right way.