The Span That Cuts Allocations in High-Performance C#
For years, writing genuinely high-performance C# meant fighting the allocator. Every substring, every array slice, every buffer copy created a new object on the heap, and all those objects eventually had to be collected, producing the garbage-collection pauses that undermine latency-sensitive applications. Then a type arrived that changed the equation: Span. It is one of the most important additions to modern .NET for performance-critical code, and yet it remains poorly understood, wrapped in constraints that seem arbitrary until you see the reasoning. Understanding what Span does, and why it works the way it does, is essential for anyone writing code where allocations matter.

The cost that Span eliminates
To appreciate Span, you have to appreciate the problem it solves. In everyday C#, working with part of a larger piece of data usually means creating a copy. Taking a substring allocates a new string; slicing an array allocates a new array; extracting a portion of a buffer copies bytes into a fresh one. Each of these operations produces a new object on the managed heap, and while any single allocation is cheap, the accumulation in hot code paths is not. Those objects must be tracked and eventually reclaimed by the garbage collector, whose work introduces pauses and consumes CPU.
For most applications this cost is negligible and not worth thinking about. But in high-throughput or low-latency scenarios — parsing, serialization, network processing, anything running millions of times a second — the allocations add up to real garbage-collection pressure, and the pauses become a genuine performance problem. The traditional ways of avoiding this were awkward and error-prone, involving manual buffer management and unsafe code. Span exists to give this performance without that awkwardness: a way to work with slices of data without allocating anything at all, keeping the hot path free of the garbage it used to generate.
What a Span actually is
A Span is, at its core, a view over a contiguous region of memory — a way to reference a slice of existing data without copying it. Instead of allocating a new object to hold part of an array or buffer, a Span points at a portion of the original memory and lets you read and write it directly, as if it were its own array. Slicing a Span produces another Span pointing at a sub-region, again without copying. The data is never duplicated; only the view changes.
This is the key to how it cuts allocations. When you take a slice of a Span, no new array is created and nothing is placed on the heap; you get a new view over the same underlying memory. An operation that would traditionally allocate a substring or a sub-array instead produces a Span that simply refers to the relevant part of the existing data. The work happens in place, on memory that already exists, which means the garbage collector has nothing new to track. For code that spends its time carving up and processing buffers, this transforms a stream of allocations into no allocations at all, which is exactly what latency-sensitive code needs.
Why Span comes with strict rules
Span is unusual in that it carries constraints most C# types do not, and these constraints puzzle developers until they understand the reasoning. A Span cannot be stored on the heap — it cannot be a field of a class, cannot be captured in certain closures, cannot be used across an await, and lives only on the stack. These restrictions are not arbitrary; they are the price of Span's safety and performance, and they follow directly from what it is.
The reasoning is about lifetime and safety. Because a Span is a direct view into memory it does not own, it must not outlive the memory it points at, or it would become a dangling reference into freed or moved data — exactly the kind of unsafe situation managed code exists to prevent. By confining Span to the stack, the language guarantees that its lifetime is bounded and cannot escape into contexts where the underlying memory might disappear beneath it. The constraints are what make Span safe to use without the dangers of manual memory management: you get the performance of working directly with memory and the safety of the managed runtime, and the stack-only rule is what reconciles the two. Once you see that, the restrictions stop feeling arbitrary and start feeling like the guardrails that make the whole thing possible.
Where Span fits, and where it does not
Understanding Span's constraints also clarifies where it belongs. It shines in synchronous, performance-critical code that processes data in place — parsers, formatters, serializers, buffer manipulation, and the tight loops of high-throughput systems. In these contexts, its ability to slice and process memory without allocating delivers meaningful, measurable improvements, cutting garbage-collection pressure exactly where it hurts. This is the code Span was designed for, and using it there is one of the most effective performance techniques available in modern .NET.
Where Span does not fit is equally important to recognise. Because it cannot be stored on the heap or used across asynchronous boundaries, it is unsuitable for scenarios that need to hold a view over memory beyond a single stack frame or across an await — for which related types designed to live on the heap exist instead. And in ordinary application code where allocations are not a bottleneck, reaching for Span adds complexity and constraint for no real benefit; the readable, allocating approach is the right choice when performance is not the constraint. The skill is knowing when the allocation cost actually matters, and applying Span there rather than everywhere. Like most performance tools, it is a targeted instrument, not a default, and using it indiscriminately trades clarity for a benefit that is often not needed.
Measuring before reaching for it
A final and important discipline surrounds Span, as it does all performance work: measure before optimising. The allocations Span eliminates are only worth eliminating where they are actually a problem, and that is a matter of evidence, not assumption. In a great deal of code, the garbage collector handles allocations so efficiently that removing them yields no perceptible benefit, and complicating the code with Span in pursuit of a non-existent gain is a net loss. The value of Span is real, but it is realised only where allocation pressure is genuinely limiting performance.
The sound approach, therefore, is to identify the hot paths where allocations demonstrably hurt — through profiling and measurement — and to apply Span there, deliberately, where its benefit is proven. This is the same evidence-driven mindset that governs all sensible optimisation: understand where the cost actually lies before spending complexity to reduce it. Applied that way, Span becomes a precise tool for the specific problem of allocation pressure in performance-critical code, rather than a source of premature complexity spread across a codebase that never needed it. The measurement is what turns Span from a fashionable technique into a genuine improvement.
Conclusion
Span is one of modern .NET's most valuable tools for high-performance code, and its value lies in a single capability: working with slices of memory without allocating, eliminating the garbage-collection pressure that traditionally came from copying substrings, sub-arrays, and buffers in hot paths. It achieves this by being a view over existing memory rather than a copy of it, so that slicing and processing happen in place with nothing new on the heap. Its stack-only constraints, which seem arbitrary at first, are precisely what make this safe, bounding its lifetime so it can never dangle over memory that has disappeared. Span belongs in synchronous, performance-critical code that processes data in place, not in ordinary code where allocations do not matter, and it should be applied where measurement shows allocation pressure genuinely limits performance. Understood and used with that discipline, Span delivers the rare combination the platform long lacked — the speed of direct memory access with the safety of managed code — exactly where it counts.


