100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET

Span<T> and Memory<T>

How Span<T> and Memory<T> let .NET code work with contiguous memory safely and with minimal or zero allocations, and when to use each.

PerformanceAdvanced11 min readJul 10, 2026
Analogies

What Span<T> and Memory<T> Solve

Span<T> is a ref struct that represents a contiguous, type-safe view over a region of memory, whether that memory lives in a managed array, a stack-allocated buffer, or unmanaged memory. Before Span<T>, slicing a substring or array segment usually meant allocating a new copy (string.Substring, Array.Copy), which piles up garbage in hot paths like parsers or network protocol handlers. Because Span<T> is a ref struct, it can only live on the stack, which lets the runtime guarantee memory safety without needing GC tracking for the span itself, while still letting you index into the underlying buffer with bounds checking and zero additional allocation.

🏏

Cricket analogy: It's like a coach reviewing a specific over of a match on the broadcast feed without re-recording that footage into a new file; Span<T> lets you 'view' a slice of a buffer the same way, without copying the underlying data.

Memory<T> for Heap-Friendly, Async-Friendly Scenarios

Because Span<T> is a ref struct, it cannot be stored in a field of a class, boxed, used as a generic type argument, or captured in an async method's state machine, since those all require heap storage or crossing await boundaries. Memory<T> exists for exactly those cases: it is a regular struct wrapping a reference to an array, a MemoryManager<T>, or similar, and it can be stored on the heap, passed across await points, and converted to a Span<T> on demand via its .Span property when you actually need to read or write. A typical pattern is exposing a Memory<byte> from an async Stream.ReadAsync overload, then calling .Span inside a synchronous helper once the data has arrived.

🏏

Cricket analogy: It's like the difference between watching live action pitch-side (Span, only valid in the moment) versus a match highlights package you can store and replay anytime later (Memory), which you convert back to 'live view' when you're ready to analyze it.

Stack Allocation with stackalloc and ref structs

Span<T> pairs naturally with stackalloc, which allocates a fixed-size block directly on the stack instead of the managed heap, avoiding GC pressure entirely for small, short-lived buffers such as parsing a handful of bytes from a socket header. Because stack memory is reclaimed automatically when the method returns, and Span<T>'s ref-struct nature prevents it from escaping that method's lifetime (you cannot return a Span<T> wrapping a stackalloc'd buffer from the method that created it, and the compiler enforces this via lifetime/escape analysis), the combination is both fast and provably safe. This is why System.Text.Json and high-performance HTTP parsers in Kestrel lean heavily on Span<T> and stackalloc for zero-allocation parsing.

🏏

Cricket analogy: It's like a bowler chalking a temporary run-up mark on the pitch before an over that gets swept away by ground staff right after; stackalloc memory exists only for the duration of the method, just like that chalk mark only lasts the over.

csharp
// Zero-allocation parsing using Span<T> and stackalloc
public static bool TryParseHeader(ReadOnlySpan<byte> packet, out int messageLength)
{
    // Slice without copying: a view into the original buffer
    ReadOnlySpan<byte> lengthField = packet.Slice(0, 4);
    messageLength = BinaryPrimitives.ReadInt32BigEndian(lengthField);
    return messageLength > 0 && messageLength <= packet.Length - 4;
}

public static async Task<byte[]> ReadFrameAsync(Stream stream)
{
    // Memory<T> can cross the await boundary; Span<T> cannot.
    Memory<byte> buffer = new byte[4096];
    int read = await stream.ReadAsync(buffer);
    Span<byte> received = buffer.Span.Slice(0, read);

    Span<byte> scratch = stackalloc byte[16]; // stack-only, zero GC pressure
    received.Slice(0, 16).CopyTo(scratch);
    return received.ToArray();
}

Never store a Span<T> as a field on a class, use it as a generic type parameter, or try to keep it alive across an await. The compiler will reject most of these outright because Span<T> is a ref struct, but the underlying reason matters: it exists purely as a short-lived, stack-bound view, and Memory<T> is the tool for anything longer-lived.

System.Text.Json's Utf8JsonReader is built entirely around ReadOnlySpan<byte>, which is a major reason it dramatically outperforms Newtonsoft.Json's string-based parsing on large payloads: it never allocates copies just to tokenize input.

  • Span<T> is a ref struct providing a stack-only, allocation-free, type-safe view over contiguous memory.
  • Slicing a Span<T> or ReadOnlySpan<T> does not copy the underlying data, unlike string.Substring or Array.Copy.
  • Memory<T> is a heap-friendly counterpart that can be stored in fields and crossed over await boundaries; call .Span to get a Span<T> when you need to read or write.
  • stackalloc allocates a fixed-size buffer directly on the stack, avoiding GC pressure for small, short-lived data.
  • The compiler enforces that a Span<T> can never outlive the stack frame that created it, preventing dangling references.
  • High-performance libraries like System.Text.Json and ASP.NET Core Kestrel rely heavily on Span<T> for zero-allocation parsing.
  • Use Memory<T> for async APIs and long-lived storage; use Span<T> for synchronous, in-method processing.

Practice what you learned

Was this page helpful?

Topics covered

#NET#NETCoreStudyNotes#MicrosoftTechnologies#SpanTAndMemoryT#Span#Memory#Solve#Heap#StudyNotes#SkillVeris