async programming
TcpClient
C#
asynchronous
network programming

When to using async when dealing with TcpClients?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Use async with TcpClient whenever you are handling more than one connection, building a server, or running in a UI application. Synchronous TCP calls block the calling thread while waiting for data to arrive over the network, often for milliseconds to seconds per call. With async, the thread is released during the wait, letting your application handle thousands of concurrent connections on a small thread pool or keep a UI responsive.

Synchronous vs Async

csharp
1// SYNCHRONOUS: blocks the thread during network I/O
2using var client = new TcpClient();
3client.Connect("example.com", 80);  // Blocks until connected
4NetworkStream stream = client.GetStream();
5
6byte[] data = Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n");
7stream.Write(data, 0, data.Length);  // Blocks until sent
8
9byte[] buffer = new byte[4096];
10int bytesRead = stream.Read(buffer, 0, buffer.Length);  // Blocks until data arrives
csharp
1// ASYNC: releases the thread during network I/O
2using var client = new TcpClient();
3await client.ConnectAsync("example.com", 80);  // Thread released during connect
4NetworkStream stream = client.GetStream();
5
6byte[] data = Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n");
7await stream.WriteAsync(data, 0, data.Length);  // Thread released during send
8
9byte[] buffer = new byte[4096];
10int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);  // Thread released

The async version uses the same API but prefixed with Async and await. The thread is free to do other work during each network operation.

When to Use Async

1. Server Handling Multiple Clients

csharp
1// Async server: handles thousands of connections on a few threads
2var listener = new TcpListener(IPAddress.Any, 8080);
3listener.Start();
4
5while (true)
6{
7    TcpClient client = await listener.AcceptTcpClientAsync();
8    _ = HandleClientAsync(client);  // Fire and forget, each client runs concurrently
9}
10
11async Task HandleClientAsync(TcpClient client)
12{
13    using (client)
14    {
15        var stream = client.GetStream();
16        var buffer = new byte[4096];
17        int bytesRead;
18
19        while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
20        {
21            // Echo back
22            await stream.WriteAsync(buffer, 0, bytesRead);
23        }
24    }
25}

A synchronous server would need one thread per client. With 1,000 clients, that is 1,000 blocked threads. Async handles them all with a handful of thread pool threads.

2. UI Applications (WPF, WinForms, MAUI)

csharp
1// WRONG: freezes the UI
2private void ConnectButton_Click(object sender, EventArgs e)
3{
4    var client = new TcpClient();
5    client.Connect("server.com", 9000);  // UI frozen for seconds
6    StatusLabel.Text = "Connected";
7}
8
9// CORRECT: UI stays responsive
10private async void ConnectButton_Click(object sender, EventArgs e)
11{
12    var client = new TcpClient();
13    StatusLabel.Text = "Connecting...";
14    await client.ConnectAsync("server.com", 9000);  // UI remains responsive
15    StatusLabel.Text = "Connected";
16}

3. Multiple Concurrent Connections

csharp
1// Fetch from 10 servers in parallel
2var tasks = servers.Select(async server =>
3{
4    using var client = new TcpClient();
5    await client.ConnectAsync(server.Host, server.Port);
6    var stream = client.GetStream();
7    await stream.WriteAsync(requestData);
8    return await ReadResponseAsync(stream);
9});
10
11var responses = await Task.WhenAll(tasks);
12// All 10 complete in roughly 1 round-trip time, not 10x

When Synchronous Is Acceptable

csharp
1// 1. Simple console tool making one connection
2static void Main()
3{
4    using var client = new TcpClient("server.com", 9000);
5    // Only one connection, no UI, no concurrency needed
6    // Blocking is fine here
7}
8
9// 2. Background thread dedicated to one connection
10Thread worker = new Thread(() =>
11{
12    using var client = new TcpClient("server.com", 9000);
13    var stream = client.GetStream();
14    while (true)
15    {
16        int b = stream.ReadByte();  // Blocking is OK, dedicated thread
17        Process(b);
18    }
19});
20worker.IsBackground = true;
21worker.Start();

Synchronous is acceptable when you have a single connection on a dedicated thread with no UI to block.

Async with Timeout and Cancellation

csharp
1async Task<string> FetchWithTimeoutAsync(string host, int port, CancellationToken ct)
2{
3    using var client = new TcpClient();
4    using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
5    timeoutCts.CancelAfter(TimeSpan.FromSeconds(10));
6
7    await client.ConnectAsync(host, port, timeoutCts.Token);
8
9    var stream = client.GetStream();
10    var buffer = new byte[4096];
11    var bytesRead = await stream.ReadAsync(buffer, timeoutCts.Token);
12
13    return Encoding.UTF8.GetString(buffer, 0, bytesRead);
14}

CancellationToken lets you cancel long-running operations cleanly. This is essential for production TCP code.

Async Read Loop Pattern

csharp
1async Task ReadLoopAsync(NetworkStream stream, CancellationToken ct)
2{
3    var buffer = new byte[4096];
4    var messageBuffer = new MemoryStream();
5
6    while (!ct.IsCancellationRequested)
7    {
8        int bytesRead = await stream.ReadAsync(buffer, ct);
9        if (bytesRead == 0)
10            break;  // Connection closed by remote
11
12        messageBuffer.Write(buffer, 0, bytesRead);
13
14        // Process complete messages
15        while (TryParseMessage(messageBuffer, out var message))
16        {
17            await ProcessMessageAsync(message);
18        }
19    }
20}

This pattern reads data as it arrives without blocking a thread between packets.

Common Pitfalls

  • Mixing sync and async: Calling .Result or .Wait() on async TCP operations causes deadlocks in UI apps and ASP.NET. Always use await end-to-end.
  • Fire-and-forget without error handling: _ = HandleClientAsync(client) silently swallows exceptions. Wrap in try/catch or use Task.Run with error logging.
  • Not using CancellationToken: Without timeouts, ReadAsync waits forever if the remote side goes silent. Always pass a CancellationToken with a timeout.
  • TcpClient.Connected is unreliable: This property only reflects the last known state. A connection can drop between checking Connected and calling ReadAsync. Handle IOException and SocketException instead.
  • Buffering issues: NetworkStream.ReadAsync may return fewer bytes than requested (partial reads). Always loop until you have a complete message, using a length prefix or delimiter protocol.

Summary

  • Use async TCP when handling multiple connections, in UI apps, or in ASP.NET
  • ConnectAsync, ReadAsync, WriteAsync release the thread during network waits
  • Async servers handle thousands of connections on a few thread pool threads
  • Synchronous is fine for single-connection console tools on dedicated threads
  • Always use CancellationToken with timeouts for production TCP code
  • Handle partial reads because ReadAsync does not guarantee a complete message per call

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

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

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.