Stream
CopyToAsync
asynchronous programming
troubleshooting
.NET

Stream CopyToAsync never returns

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Stream.CopyToAsync hanging indefinitely is almost always caused by a deadlock from blocking on async code (calling .Result or .Wait() on the task) or by the source stream never signaling end-of-data. The fix depends on the root cause: use await instead of .Result, set HttpCompletionOption.ResponseHeadersRead for HTTP streams, or ensure the source stream has a finite length. This issue is especially common in ASP.NET, WPF, and WinForms applications where the SynchronizationContext serializes continuations onto a single thread.

The Deadlock Pattern

csharp
1// THIS DEADLOCKS in ASP.NET / WPF / WinForms
2public byte[] GetData()
3{
4    var ms = new MemoryStream();
5    // .Result blocks the current thread, which holds the SynchronizationContext
6    // CopyToAsync's continuation needs that same thread → deadlock
7    sourceStream.CopyToAsync(ms).Result;
8    return ms.ToArray();
9}
csharp
1// FIX: Use async all the way up
2public async Task<byte[]> GetDataAsync()
3{
4    var ms = new MemoryStream();
5    await sourceStream.CopyToAsync(ms);
6    return ms.ToArray();
7}

The SynchronizationContext in ASP.NET (pre-Core), WPF, and WinForms captures the current thread for continuations. When you call .Result, you block that thread. The await continuation in CopyToAsync needs to resume on that same thread, but it is blocked — deadlock.

ConfigureAwait(false) Workaround

csharp
1// If you cannot make the caller async, use ConfigureAwait(false)
2public byte[] GetData()
3{
4    var ms = new MemoryStream();
5    sourceStream.CopyToAsync(ms).ConfigureAwait(false).GetAwaiter().GetResult();
6    return ms.ToArray();
7}
8
9// Better: use async throughout (preferred approach)
10public async Task<byte[]> GetDataAsync()
11{
12    var ms = new MemoryStream();
13    await sourceStream.CopyToAsync(ms).ConfigureAwait(false);
14    return ms.ToArray();
15}

ConfigureAwait(false) tells the continuation not to marshal back to the original context, avoiding the deadlock. However, the proper fix is to use async/await throughout the call chain.

HTTP Response Stream Hanging

csharp
1// THIS MAY HANG: HttpClient buffers the entire response by default
2var response = await client.GetAsync(url);
3var ms = new MemoryStream();
4await response.Content.CopyToAsync(ms);  // Hangs if response is huge
5
6// FIX: Use ResponseHeadersRead to stream the response
7var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
8using var stream = await response.Content.ReadAsStreamAsync();
9var ms = new MemoryStream();
10await stream.CopyToAsync(ms);

With the default HttpCompletionOption.ResponseContentRead, GetAsync buffers the entire response body before returning. For large responses, this can cause memory issues or apparent hangs. ResponseHeadersRead returns as soon as headers arrive, letting you stream the body incrementally.

Source Stream Never Ends

csharp
1// CopyToAsync reads until the source returns 0 bytes
2// If the source never signals end-of-stream, CopyToAsync never returns
3
4// Problem: NetworkStream stays open
5var networkStream = tcpClient.GetStream();
6var ms = new MemoryStream();
7await networkStream.CopyToAsync(ms);  // Hangs until the remote side closes
8
9// Fix 1: Use a CancellationToken with timeout
10var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
11try
12{
13    await networkStream.CopyToAsync(ms, cts.Token);
14}
15catch (OperationCanceledException)
16{
17    Console.WriteLine("Copy timed out");
18}
19
20// Fix 2: Read a known number of bytes instead
21var buffer = new byte[contentLength];
22int totalRead = 0;
23while (totalRead < contentLength)
24{
25    int read = await networkStream.ReadAsync(
26        buffer, totalRead, contentLength - totalRead);
27    if (read == 0) break;
28    totalRead += read;
29}

GZipStream / DeflateStream Issues

csharp
1// Problem: GZipStream must be closed/flushed before reading the output
2var compressed = new MemoryStream();
3using (var gzip = new GZipStream(compressed, CompressionMode.Compress, leaveOpen: true))
4{
5    await sourceStream.CopyToAsync(gzip);
6}  // GZipStream writes final bytes on Dispose
7
8compressed.Position = 0;
9// Now compressed contains the full gzip data
10
11// Common mistake: reading compressed before closing GZipStream
12var gzip = new GZipStream(compressed, CompressionMode.Compress);
13await sourceStream.CopyToAsync(gzip);
14compressed.Position = 0;  // Missing final bytes — output is truncated or corrupt

Proper Pattern with CancellationToken

csharp
1public async Task CopyWithProgressAsync(
2    Stream source, Stream destination,
3    IProgress<long> progress = null,
4    CancellationToken ct = default,
5    int bufferSize = 81920)
6{
7    var buffer = new byte[bufferSize];
8    long totalBytes = 0;
9    int bytesRead;
10
11    while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
12    {
13        await destination.WriteAsync(buffer, 0, bytesRead, ct);
14        totalBytes += bytesRead;
15        progress?.Report(totalBytes);
16    }
17}
18
19// Usage
20var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
21var progress = new Progress<long>(bytes => Console.WriteLine($"Copied {bytes} bytes"));
22await CopyWithProgressAsync(source, dest, progress, cts.Token);

Common Pitfalls

  • Calling .Result or .Wait() on CopyToAsync in UI or ASP.NET contexts: This causes a classic deadlock. The calling thread holds the SynchronizationContext, and the async continuation needs that same context to resume. Use await throughout the call chain, or as a last resort, use ConfigureAwait(false) before .GetAwaiter().GetResult().
  • Not setting HttpCompletionOption.ResponseHeadersRead for large downloads: The default behavior buffers the entire HTTP response into memory before CopyToAsync starts. For large files, this looks like a hang while the buffer fills. Use ResponseHeadersRead to start streaming immediately.
  • CopyToAsync on a stream that never signals end-of-data: Network streams, named pipes, and console input streams may never return 0 bytes from ReadAsync. Without a CancellationToken or known content length, CopyToAsync waits forever. Always pass a cancellation token with a timeout for network streams.
  • Forgetting to reset MemoryStream.Position after writing: After CopyToAsync writes to a MemoryStream, the position is at the end. Reading from it without resetting position to 0 returns nothing, making it appear empty. Call ms.Position = 0 or use ms.ToArray() to get all bytes regardless of position.
  • Not disposing GZipStream/DeflateStream before reading output: Compression streams write final bytes (footer, checksum) during Dispose/Close. If you read the destination stream before the compression stream is disposed, the output is incomplete. Use a using block to ensure the compression stream flushes completely.

Summary

  • The most common cause of CopyToAsync hanging is blocking with .Result or .Wait() in a SynchronizationContext — use await instead
  • For HTTP streams, use HttpCompletionOption.ResponseHeadersRead to avoid buffering the entire response
  • Always pass a CancellationToken with a timeout when copying from network streams
  • Reset MemoryStream.Position to 0 after writing before reading
  • Dispose compression streams before reading their output to ensure all bytes are flushed

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.