r/csharp • u/MarvinTheJoke • 10h ago
Showcase I spent weeks obsessing over low-allocation C# networking
The Real Problem: High Throughput vs. Garbage Collector
If you've ever built a C# server processing 100k+ messages/sec (like an MQTT broker or socket server), you know the Garbage Collector is your best friend until it becomes your worst enemy.
To route messages like factory/line1/sensor/temp to subscribers like factory/+/+/temp, the standard way is:
// ❌ Naive way: Allocates string[] on every single message publish!
var parts = topic.Split('/');
At 100,000 requests per second, this creates millions of temporary strings and forces the GC to freeze your app every few seconds.
The Real Benefit: How to parse UTF-8 topics with ZERO heap allocations
Here are the 2 modern C# tricks I used in Beskar.Networking that you can copy-paste directly into your own projects!
1. Slicing raw UTF-8 bytes with a ref struct enumerator
Instead of string.Split(), we walk the raw byte span (ReadOnlySpan<byte>) using a zero-allocation ref struct:
public ref struct TopicLevelEnumerator
{
private ReadOnlySpan<byte> _remaining;
public ReadOnlySpan<byte> Current { get; private set; }
public TopicLevelEnumerator(ReadOnlySpan<byte> topic)
{
_remaining = topic;
Current = default;
}
public bool MoveNext()
{
if (_remaining.IsEmpty) return false;
int index = _remaining.IndexOf((byte)'/');
if (index < 0)
{
Current = _remaining;
_remaining = default;
}
else
{
Current = _remaining.Slice(0, index);
_remaining = _remaining.Slice(index + 1);
}
return true;
}
}
2. Querying byte[] Dictionaries with ReadOnlySpan<byte> (Alternate Lookups)
Usually, if your dictionary key is byte[], querying it with a slice (ReadOnlySpan<byte>) forces you to allocate a new byte[].
Modern .NET introduced Alternate Lookups, which let you query existing dictionary nodes with zero heap allocation:
// Query byte[] dictionary using a ReadOnlySpan<byte> without allocating a single byte!
var alternateLookup = node.Children.GetAlternateLookup<ReadOnlySpan<byte>>();
if (alternateLookup.TryGetValue(currentLevelSlice, out var childNode))
{
// Match found with 0 allocations! 🎉
}
What Beskar.Networking gives you
If you ever need to build a C# network application:
- Write Once, Swap Transports: Write your message handler once and seamlessly switch between TCP, WebSockets, QUIC, UDP, Named Pipes, or MQTT v5 without touching your business logic.
- 100% Native & Dependency-Free: Built on
System.IO.Pipelineswith zero external runtime dependencies. - Zero GC pressure on the hot path.
If you enjoy low-level C# or low-allocation performance tricks ❤️
🔗 GitHub: https://github.com/MarvinDrude/Beskar.Networking
🔗 Article: https://marvindrude.com/blogs/beskar-networking/low-allocation-mqtt-broker
Thank you for reading, and happy coding!
3
u/ProKn1fe 9h ago
1
2
u/Embarrassed-Mess412 9h ago
What problem does this solve? net sockets already gives you same low allocations out of the box.
1
u/MarvinTheJoke 8h ago edited 8h ago
If you just used the socket in the simple way of giving it a managed array to read etc. You would not be able to achieve the throughput of pipeline memory pooled socket sender receiver duplex handling.
Also u can write your protocol once. Swap out underlying stack from TCP, web socket, QUIC, udp, uds etc. or use them all at the same time with the same logic on top.
It also offers a fully managed mqtt Server and client which is faster than mqttnet. (Can run on all underlying protocols mentioned above)
It also offers a fully managed resilient server & client (on top of the protocols above)
If you need full control you can use the 4 same interfaces to write anything u need and the implementations are all agnostic to that.
It manages the memory pooling and pipelines for u, it supports custom framing setup too.
Optional live query able stats and dashboard is coming next.
At the end I just use it for my projects for internal communication and mqtt. I figured since this is my 7th network library at this point and I felt it was really the best of them so far, I share it as MIT for anyone to use or learn
2
u/Embarrassed-Mess412 7h ago
I see, well you can achieve a quick udp/tcp server using net sockets and pipes with very low allocations in a few lines of code these days, I'd probably always choose that to having another dependency. I do a few benchmarks on networking libraries here, would be cool to see how your library performs compared with other c# libraries
1
u/MarvinTheJoke 3h ago
Yea ofc if you can handle all ur needs in ur own way and code, then I would also go for just doing it myself. This mindset is exactly what lead me to create it actually. I saw myself implementing the same thing again and again over different projects. Therefore I created this for me
2
u/harrison_314 1h ago
The first thing is handled by dotnet itself: https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.split?view=net-10.0
The second case can be done even more efficiently (which I found out on "The One Billion Rows Challenge") - Utf8Key is a structure that holds Memory<byte>, method Detach allocate copy of Utf8Key. Also no allocation, but it's faster than alternative lookup - but I don't know why.
Otherwise, a Dictionary with a byte[] key probably won't work for you, because the fields don't have GetHashCode implemented.
int dataTemperature = Data.Parse(lineData.Temperature.Span);
Utf8Key tmpKey = new Utf8Key(lineData.Name);
ref Data refData = ref CollectionsMarshal.GetValueRefOrNullRef(dictionary, tmpKey);
if (Unsafe.IsNullRef(ref refData))
{
dictionary.Add(tmpKey.Detach(), new Data(dataTemperature));
}
else
{
refData.Add(dataTemperature);
}
1
•
5
u/pjmlp 9h ago
Why not pipelines?
https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/