High-Performance Networking with .NET 10 - MsQuic, Sockets, and the New I/O Reality

If you want to write a .NET networking post today, the interesting work is in latency control, backpressure, memory reuse, and what happens when you push tens of thousands of concurrent connections through a real service. .NET 10 is a good moment to revisit this because the runtime and libraries keep shaving allocations and adding higher-level primitives that are finally worth using in production.
This post will build a small but serious component, a low-latency ingest gateway that can accept messages over QUIC (HTTP/3 style transport), fall back to TCP when needed, and keep CPU and allocations predictable under load. You will use System.Net.Quic (MsQuic under the hood on supported platforms), compare it against raw sockets for a tight loop protocol, and then decide where Kestrel fits when you actually want HTTP. For readers who only used QUIC via “turn on HTTP/3”, you will make QUIC feel like a tool, not magic.
What changed in .NET 10 that is interesting for networking people
The .NET team’s own networking write-up for .NET 10 is worth anchoring on because it calls out improvements across HTTP, sockets, and new WebSocket APIs. Thats interesting because networking performance is often death by a thousand small costs rather than one big fix.
Two specific areas you can lean on in a high-signal post:
HTTP/3 in ASP.NET Core and Kestrel is no longer just an experiment for most teams. If you are building mobile-facing or variable network clients, QUIC’s connection migration is a real advantage you can measure.
Library-level networking work continues to reduce allocations and improve hot paths. That changes the trade off between “DIY sockets” and “use the platform”.
The architecture you will build
You are going to build an ingest gateway with two listeners:
A QUIC listener that speaks a tiny binary protocol. It accepts a bidirectional stream per request, reads a length-prefixed frame, validates a small header, and forwards the payload to a channel for processing.
A TCP listener that speaks the same protocol for environments where QUIC is blocked, or where you do not control the client stack.
The important part is not the protocol. It is what you do around it, pooling, backpressure, cancellation, and timing. If your gateway collapses under slow clients or bursts, the protocol is irrelevant.
QUIC in .NET without the training wheels
Kestrel HTTP/3 is great when you want HTTP. But if your goal is “fast, cheap, predictable framing”, QUIC streams are a better match than HTTP request parsing. The point of this section is to show QUIC as a general transport primitive.
Here is a minimal QUIC server loop using System.Net.Quic. This is intentionally small so you can expand it into something production-level.
using System.Buffers;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Channels;
public sealed class QuicIngestServer
{
private readonly Channel<ReadOnlyMemory<byte>> _inbox;
public QuicIngestServer(Channel<ReadOnlyMemory<byte>> inbox) => _inbox = inbox;
public async Task RunAsync(IPEndPoint endPoint, X509Certificate2 cert, CancellationToken stopToken)
{
var ssl = new SslServerAuthenticationOptions
{
ServerCertificate = cert,
ApplicationProtocols = [new SslApplicationProtocol("ingest-v1")]
};
var options = new QuicListenerOptions
{
ListenEndPoint = endPoint,
ApplicationProtocols = ssl.ApplicationProtocols,
ConnectionOptionsCallback = (_, _, _) =>
ValueTask.FromResult(new QuicServerConnectionOptions
{
ServerAuthenticationOptions = ssl
})
};
await using var listener = await QuicListener.ListenAsync(options, stopToken);
while (!stopToken.IsCancellationRequested)
{
QuicConnection connection = await listener.AcceptConnectionAsync(ct);
_ = Task.Run(() => HandleConnectionAsync(connection, stopToken, stopToken);
}
}
private async Task HandleConnectionAsync(QuicConnection connection, CancellationToken stopToken)
{
await using (connection)
{
while (!stopToken.IsCancellationRequested)
{
QuicStream stream;
try
{
stream = await connection.AcceptInboundStreamAsync(stopToken);
}
catch
{
return;
}
_ = Task.Run(() => HandleStreamAsync(stream, stopToken), stopToken);
}
}
}
private async Task HandleStreamAsync(QuicStream stream, CancellationToken stopToken)
{
await using (stream)
{
byte[] lenBuf = ArrayPool<byte>.Shared.Rent(4);
try
{
await ReadExactAsync(stream, lenBuf.AsMemory(0, 4), stopToken);
int len = BitConverter.ToInt32(lenBuf, 0);
if (len <= 0 || len > 1_000_000) return;
byte[] payload = ArrayPool<byte>.Shared.Rent(len);
try
{
await ReadExactAsync(stream, payload.AsMemory(0, len), stopToken);
// Backpressure lives here.
// If downstream is slow, this will naturally throttle readers.
await _inbox.Writer.WriteAsync(payload.AsMemory(0, len), stopToken);
}
finally
{
ArrayPool<byte>.Shared.Return(payload);
}
}
finally
{
ArrayPool<byte>.Shared.Return(lenBuf);
}
}
}
private static async Task ReadExactAsync(QuicStream stream, Memory<byte> buffer, CancellationToken stopToken)
{
int read = 0;
while (read < buffer.Length)
{
int n = await stream.ReadAsync(buffer.Slice(read), stopToken);
if (n == 0) throw new InvalidOperationException("Peer closed stream early.");
read += n;
}
}
}
What makes this cool is what you do next:
You measure stream-per-request versus stream-reuse and show where head-of-line blocking goes away compared to TCP. You show what happens when you accept 20,000 connections, then start killing networks on the client side. You tie the observed behaviour back to QUIC features like multiplexed streams and, for HTTP/3 scenarios, connection migration.
TCP sockets still matter, but the rules change
Raw sockets are still the baseline for lowest overhead. The mistake most posts make is pretending sockets are always faster in real apps. They can be, but only if you also own everything around them, buffers, framing, concurrency, and pressure.
In your TCP version, you should make one hard point, you do not await ReceiveAsync forever and hope. You implement backpressure with bounded channels, you cap per-connection memory, and you drop slow consumers deliberately. When you do that, raw sockets stop being mysterious and start being comparable.
If you want to keep the post tight, do not paste a full socket server. Instead, show the two hottest sections:
the framing read loop that uses
Socket.ReceiveAsyncinto pooled buffersthe dispatcher that enforces a bounded queue and applies policy when full
Then compare those two hot sections to the QUIC stream handler above. That is the comparison people care about.
Where Kestrel fits in a performance discussion
Kestrel is not slow, its just doing more. It gives you TLS, HTTP parsing, routing, middleware, logging hooks, and observability integration. The post should explain that if you want HTTP, you should not throw them away for a custom protocol unless you can prove the win in your own workload.
If you do want HTTP/3, show the minimum config to enable it and then measure it. The Microsoft doc on HTTP/3 in Kestrel is the authoritative reference here, including QUIC behaviour like connection migration.
A clean way to structure this section is to treat Kestrel as a control layer and QUIC streams or TCP sockets as a data layer. Your control layer can be normal HTTP, OpenAPI, auth, throttling rules, and config updates. Your data layer can be the ultra-lean protocol used for high-rate ingest.
WebSockets in .NET 10 - the bit most people will miss
A lot of systems still use WebSockets for realtime feeds because the client framework is easy. .NET 10 added new WebSocket APIs that change how you can shape streaming code, and this is exactly the kind of thing that will not be done to death yet. It is called out explicitly in the .NET 10 networking improvements material and in coverage of the release. If you include WebSockets, do it with purpose, show one scenario where WebSockets is the right choice for browser clients, then show your QUIC protocol for service-to-service.
How to benchmark this
If you publish one graph and call it a day, senior readers will ignore you. Do three measurements and explain what they mean:
Throughput - messages per second at fixed payload sizes.
Tail latency - p95 and p99 under burst and under slow clients.
CPU cost - cores consumed at target throughput.
Then use two test frames:
A steady load that fits in cache and shows best case.
A bursty load with induced jitter, where your backpressure and queue policy is the real differentiator.
Finally, explicitly call out your environment and settings: Linux vs Windows, container limits, TLS on or off, payload sizes, and number of concurrent connections. Without those, your results are just marketing.
End with a concrete rule:
If you need HTTP and broad compatibility, use Kestrel, enable HTTP/3 where it helps, and focus on the app-level bottlenecks.
If you need predictable low-latency ingest between controlled clients and services, QUIC streams are now practical in .NET, and they remove entire classes of TCP pain while staying fast.






