ReadOnlyMemory and ReadOnlySpan Across Async Boundaries
Choose ReadOnlySpan for short-lived views and ReadOnlyMemory across await.
The caller times out, but its consumer task is still pending. May the producer return the rented array? Work through the A–D timeline before checking the answers. The view type, the caller’s wait and the buffer lease are separate contracts.
Two views, one borrowed buffer
Think of a span as a lightweight view of a region, not a copy or a transfer of ownership. A span referring to managed memory participates in GC tracking, but it does not enforce a pool lease or prevent another owner from returning a rented buffer. A ReadOnlySpan<T> is a ref struct, which is why it behaves like a stack-only view. That design makes it ideal for short-lived parsing, slicing, and inspection inside a single synchronous flow, and compiler ref-safety rules prevent references to shorter-lived storage from escaping. A span over valid heap-backed storage can be returned from a method; a span over that method's stack-allocated storage cannot.
ReadOnlyMemory<T> solves a different problem. It is also a view over a contiguous region, but it is not a byref-like type, so it can live on the managed heap and be stored in fields, returned from methods, or carried across asynchronous work. That does not mean ownership changes. A ReadOnlyMemory<T> instance can still point at an array rented from a pool, and the owner of that rented array is still the component that must dispose or return it.
A useful mental model is this timeline:
Producer rents buffer -> fills data -> hands out ReadOnlyMemory<T> -> async consumer awaits -> consumer reads later -> producer/owner returns buffer only after all consumers finish
The key distinction is that holding a memory view is not the same as owning the buffer. If a rented array is returned to the pool too early, later consumers may observe corrupted or unrelated data.
The view does not extend the lease
ReadOnlySpan<T> is optimized for synchronous, short-lived use. Because it is a ref struct, it cannot be boxed, captured by a lambda, stored in a field of a normal class, or preserved across an await in the way a heap object can. Even though newer C# versions have relaxed some local-variable restrictions in async methods, the important interview answer is unchanged: a span must not be preserved across an await boundary.
ReadOnlyMemory<T> is the async-friendly sibling. You can use it when a method needs to keep a view around while it awaits I/O or other asynchronous work. However, the view still references the same underlying storage, so the storage must remain valid for the entire lease of every consumer.
This leads to the ownership rule that interviewers care about:
- Owner: responsible for lifetime management of the actual buffer.
- Consumer: reads or processes the buffer.
- Lease: the time window during which the consumer is allowed to use it.
In practice, if you rent an array from a pool and pass a ReadOnlyMemory<T> to an async consumer, you must keep the array rented until that consumer is done. ReadOnlyMemory<T> does not transfer ownership, and it does not make the array immutable. It only makes the view read-only.
That distinction matters because the backing storage might still be mutable through another reference. ReadOnlyMemory<T> prevents mutation through that specific API surface, but it does not magically freeze the underlying buffer.
Worked lease timeline
This is a hypothetical reasoning exercise. A producer rents one byte array and lends a ReadOnlyMemory<byte> view to one async consumer. The producer remains the owner. Assume the consumer's returned Task covers every use of that array, including any work it starts, and there are no other consumers.
- A: Work starts. The consumer suspends at an
await. Its task is incomplete, and it may read the array when it resumes. - B: Cancellation is requested. The consumer has not responded yet. Its task is still incomplete.
- C: The consumer finishes. It stops all buffer access and completes its task as canceled.
- D: The owner cleans up. The producer's
awaitof that same task reports cancellation. The producer returns the array in itsfinallycleanup.
Questions
- Can the producer return the array at B? Why is D safe in this scenario?
- What changes if the consumer succeeds or faults at C instead?
- Suppose the producer stops waiting because a separate timeout expires at B, but the consumer's task is still incomplete. May cleanup return the array then?
- What if the consumer completes its task but leaves a detached reader using the array?
Explained answers
- Keep the rental at B. Cancellation is cooperative; requesting it does not establish that the operation has stopped. At C, this scenario establishes that buffer use has ended. At D, the owner has observed that completion and can return the array. See Microsoft's task cancellation guidance.
- The release point stays after C. Success, fault, and cancellation are all final task states. A successful
awaitcontinues normally; a faulted or canceled task makesawaitthrow. Cleanup must cover all three outcomes, which is why this scenario places the return infinallyaround the awaited consumer operation. Returning the array does not turn a failed operation into a successful one. See task completion states and awaited task outcomes. - No. The separate wait ended, but the stated consumer is still active. Keep the rental until that consumer finishes. Moving the return into a
finallythat runs when only the timeout finishes would still release the array too early. - The scenario's safety assumption is broken. D is no longer safe because the detached reader still has work to do. The consumer must account for that reader before completing its task, or finish copying into independently owned storage before releasing its use of the original array. A read-only view cannot repair an incorrect lifetime contract.
Failure modes
Watch for these interview traps:
- Treating
ReadOnlyMemory<T>as if it makes the backing data immutable. It does not. - Returning a rented array to a pool before all async consumers finish using the memory view.
- Trying to use
ReadOnlySpan<T>as if it were a durable async payload. - Confusing “read-only view” with “ownership transfer.”
- Assuming a span is always invalid in async methods; the more precise rule is that it cannot be preserved across an
awaitboundary. - Forgetting that a consumer may be different from the owner.
A good failure checklist to say out loud in an interview is:
- What owns the buffer?
- Who is allowed to consume it right now?
- Does any
awaitoutlive the current span lease? - If the buffer was rented, when is it returned?
- Could another reference still mutate the underlying storage?
Interview drill
If asked to choose between the two types, answer like this:
- Use
ReadOnlySpan<T>for fast, synchronous, stack-bound inspection. - Use
ReadOnlyMemory<T>when a view must be stored in ordinary object fields or preserved acrossawait, while keeping the underlying buffer valid for the consumer's entire lease. - If the data comes from a pool, keep ownership with the renter until every consumer has finished.
A strong explanation combines type shape and lifetime semantics: spans are byref-like stack views; memory is heap-storable view data; neither type changes who owns the buffer.
Check one decision
In the A–D scenario, the producer stops waiting at B while the represented consumer remains active. What permits the array to be returned?
Show answer
Correct answer: C
A timed-out wait and a cancellation request do not prove completion. The scenario’s task covers every consumer use; that completed lifetime is what permits cleanup.
Hint: Follow the lease rather than just the waiting caller.
Official sources
TechInterviewNotes Editorial Team · Updated 28 Oct 2026 ·