Span<T> and ReadOnlySpan<T> are ref struct types. An async method cannot keep either one across an await. Starting with C# 13, this version fails with CS4007:

public static async Task<int> FindCommaInFirstBlockAsync(
    Stream stream, CancellationToken cancellationToken)
{
    byte[] buffer = new byte[4096];
    ReadOnlySpan<byte> bytes = buffer;

    int read = await stream.ReadAsync(buffer, cancellationToken);
    return bytes[..read].IndexOf((byte)','); // CS4007
}

bytes is created before the read on purpose. The code waits to learn how many bytes arrived, then uses that span afterward. If the read suspends, the async state machine would have to keep the span, so the compiler rejects it. A read that happens to finish synchronously does not change the rule.

Keep Memory<T> across the await, then take a span for the synchronous work:

public static async Task<int> FindCommaInFirstBlockAsync(
    Stream stream, CancellationToken cancellationToken)
{
    byte[] buffer = new byte[4096];
    Memory<byte> memory = buffer;

    int read = await stream.ReadAsync(memory, cancellationToken);
    return memory.Span[..read].IndexOf((byte)',');
}

This searches only the first block read from the stream. In a loop, finish the span work before the next await. You can move that work into a synchronous helper that accepts ReadOnlySpan<byte> if it makes the boundary easier to see.

C# 13 and later permit span locals in async methods when their values are not used across an await boundary. Earlier language versions prohibit those locals in async methods. The restriction applies when a span would survive a suspension point.

Memory<T> does not own its backing buffer. If you rent a buffer from a pool, return it only after the read and any processing that uses the buffer are complete. Do not modify or reuse it while the read is in progress.