End-to-End Network Comparison

Read time 14 minutes
On this page

This page reports real end-to-end, over-the-socket measurements of Nalix against other .NET realtime stacks, all run on the same machine with the same harness in one session. Unlike the micro-benchmarks elsewhere in this section, every number here includes the kernel TCP stack, framing, dispatch, serialization, the thread pool and the client library.

Summary

  • Single-client latency: Nalix TCP has the lowest p50 of the full frameworks. At 32 B the p50 is 52.2 µs for Nalix TCP, 55.5 µs for Nalix WebSocket and 57.6 µs for Nalix AEAD, against 68.4 µs for gRPC duplex, 74.2 µs for the MagicOnion hub, 76.3 µs for SignalR and 106.4 µs for gRPC unary. At 1 KB, Nalix TCP achieves 53.6 µs against 69.2 µs for gRPC duplex, 79.4 µs for the MagicOnion hub and 83.5 µs for SignalR. The hand-written raw-socket baseline (30.9 µs / 24.0 µs) and the raw Kestrel WebSocket echo (45.0 µs / 48.5 µs) are faster.
  • Throughput with 64 clients:
    • At 32 B: gRPC duplex 77.6k ops/s, Nalix TCP 74.8k, SignalR 71.9k, Nalix WebSocket 69.1k, MagicOnion hub 67.5k, Nalix TCP AEAD 63.3k, gRPC unary 58.1k, MagicOnion unary 55.7k.
    • At 1 KB: SignalR 70.7k ops/s, gRPC duplex 68.4k, Nalix WebSocket 61.4k, Nalix TCP 60.8k, MagicOnion hub 58.2k, gRPC unary 56.9k, MagicOnion unary 54.8k, Nalix TCP AEAD 53.3k. Under this 4-core machine all frameworks are CPU-bound and sit within roughly 10–15% of each other at high concurrency.
  • Throughput with 1 and 16 clients: Nalix TCP is first at 1 client for both sizes (18.1k at 32 B, 16.1k at 1 KB). At 16 clients and 32 B, Nalix WebSocket (64.5k) and Nalix TCP (62.9k) are level with gRPC duplex (62.9k) and ahead of SignalR (61.2k).
  • Server CPU per message: Nalix TCP measures 29.3 µs at 64 clients and 32 B against 21.8 µs for SignalR and 21.9 µs for gRPC duplex, so its server is the more CPU-expensive one; its client is cheaper (19.2 µs client CPU/op vs 24.2 µs for SignalR).
  • Server allocations: 56 B per message at 32 B for Nalix TCP and Nalix AEAD, the lowest of the full frameworks (MagicOnion hub 200 B, gRPC duplex 264 B, SignalR 672 B). Only the raw Kestrel WebSocket baseline allocates less (0 B). At 1 KB Nalix TCP allocates 1,048 B vs 1,192 B for the MagicOnion hub, 1,256 B for gRPC duplex and 1,664 B for SignalR.
  • Encryption: Nalix AEAD (X25519 + ChaCha20-Poly1305) wins against gRPC over TLS on single-client latency (32 B p50 57.6 µs vs 100.4 µs for gRPC duplex TLS; at 1 KB 74.6 µs vs 103.8 µs), on 64-client throughput (63.3k vs 57.8k ops/s at 32 B; 53.3k vs 49.5k at 1 KB) and on server allocations (56 B vs 812 B at 32 B; 1,048 B vs 1,801 B at 1 KB).

Environment

ItemValue
CPUIntel Xeon Processor @ 2.10 GHz (4 vCPU, 1 thread per core)
Memory16 GB
OSUbuntu 24.04.4 LTS (Linux 6.18, cloud VM container)
Runtime.NET 10.0.x runtime (SDK 10.0.401), Release, x64 RyuJIT, TieredPGO on
GCServer GC + Concurrent GC for every process (from benchmarks/Directory.Build.props)
Nalixsource at master (Version.props 14.2.16), project references
ASP.NET Core / SignalR10.0.12 (Microsoft.AspNetCore.SignalR.Client, .Protocols.MessagePack → MessagePack 2.5.302)
gRPCGrpc.AspNetCore 2.84.0, Grpc.Net.Client 2.84.0, Google.Protobuf 3.35.1
MagicOnionMagicOnion.Server / .Client 7.11.0 (MessagePack 3.1.7)
Date2026-10-02 (All 12 libraries measured in one session)
Machine stateIdle container before the run; one uninterrupted session executing all 12 libraries

Methodology

Harness: benchmarks/Nalix.Comparison.Benchmarks (runner benchmarks/run-comparison.ps1 / run-comparison.sh). MagicOnion is in a separate executable, benchmarks/Nalix.Comparison.MagicOnion, because it needs MessagePack v3 while the SignalR MessagePack protocol is built against v2. Both executables share the same harness source file.

  • Topology: the driver starts the library’s server as a separate child process on 127.0.0.1, then runs the clients inside the driver process. The server process reports its own GC.GetTotalAllocatedBytes(precise: true), Gen0/1/2 collection counts and process CPU time between reset and report commands sent over stdin. So the allocation and CPU numbers are server-only, and they cover the whole process (I/O threads, timers and background tasks included).
  • Workload: echo. The client sends an opaque byte payload of 32 B or 1024 B, and the server returns the same bytes in a response message. Each library uses its idiomatic request/response API:
LabelWhat runs
raw-tcpBaseline: hand-written Socket echo with a 4-byte length prefix. No framework.
kestrel-wsBaseline: ASP.NET Core UseWebSockets binary echo, ClientWebSocket client.
nalix-tcpNalix MapTcp<DefaultProtocol> with a [PacketController] that echoes via a pooled response (PacketFactory<T>.Acquire() + context.Sender.SendAsync). The client uses TcpSession.RequestAsync<T>. Default options.
nalix-wsSame as above over MapWebSocket, with a WebSocketSession client.
nalix-tcp-aeadUseSecureConnections() + UseSystemControl(). The client runs HandshakeAsync() (X25519), then requests go out with WithEncrypt() and the handler has [PacketEncryption(true)], so both directions are ChaCha20-Poly1305.
signalr-ws-msgpackSignalR hub byte[] Echo(byte[]), WebSockets transport only, SkipNegotiation, MessagePack protocol, InvokeAsync<byte[]>.
grpc-unary / grpc-unary-tlsUnary rpc Unary(EchoMessage) returns (EchoMessage) with a bytes field, over h2c or HTTP/2+TLS (self-signed ECDSA P-256).
grpc-duplex / grpc-duplex-tlsOne long-lived bidirectional stream per client, with one write and one read per operation.
magiconion-unary / magiconion-hubIService<T> UnaryResult<byte[]>, or a StreamingHub method returning ValueTask<byte[]>, over h2c.
  • Every client owns its own connection (for gRPC and MagicOnion, its own GrpcChannel / SocketsHttpHandler), so “N clients” means N TCP connections for every library.
  • Latency: one client, sequential calls. 20 000 warm-up calls, then 100 000 timed calls. Every call is timed with Stopwatch and the samples are sorted to read exact percentiles.
  • Throughput: N ∈ {1, 16, 64} clients in a closed loop (each client keeps one request in flight). After 2 s of warm-up, requests are counted over an 8 s window.
  • Runs: 3 runs of each cell. The tables show the median of the runs, and ±x% is half of the (max − min)/median spread.
  • ASP.NET Core servers use WebApplication.CreateBuilder, with logging cleared and the minimum level set to Warning, and Kestrel bound only to loopback. No competitor was tuned beyond its documented default setup. The same goes for Nalix: no tuning at all (loopback is exempt from the default per-IP connection quota since #365).
  • Raw data (JSON lines, one row per run) is in data/network-comparison-results.jsonl. The tables below are produced from it by benchmarks/Nalix.Comparison.Benchmarks/aggregate.py. The file also holds two diagnostic Nalix runs (nalix-tcp-nocomp, nalix-tcp-inline) that are not part of the tables.

Reproduce with:

benchmarks/run-comparison.sh        # Linux/macOS
.\benchmarks\run-comparison.ps1    # Windows

Results

Latency — 32 B payload, 1 client, sequential request/response

Librarymean (µs)p50 (µs)p90 (µs)p99 (µs)p99.9 (µs)run spread (p50)server alloc/op (B)
raw-tcp39.430.966.0119.2540.3±11.0%144
kestrel-ws50.445.079.9132.8564.4±2.8%0
nalix-tcp59.152.284.0154.9691.9±2.8%56
nalix-ws63.355.589.0159.6654.5±2.7%192
nalix-tcp-aead64.357.691.0154.3666.4±3.3%56
signalr-ws-msgpack82.376.3116.1190.8720.1±5.8%680
grpc-unary121.1106.4170.5353.51215.7±4.1%969
grpc-duplex74.168.4101.3167.3743.1±3.9%264
grpc-unary-tls168.4150.5236.5490.81841.2±4.7%1037
grpc-duplex-tls110.5100.4155.2282.61108.5±3.6%328
magiconion-unary123.5110.2175.8379.61257.9±2.6%1433
magiconion-hub79.374.2109.1173.8784.5±8.3%200

Latency — 1024 B payload, 1 client, sequential request/response

Librarymean (µs)p50 (µs)p90 (µs)p99 (µs)p99.9 (µs)run spread (p50)server alloc/op (B)
raw-tcp33.524.057.6107.4424.1±12.0%144
kestrel-ws53.248.584.3144.5459.3±2.7%0
nalix-tcp61.253.686.8150.9760.0±6.6%1048
nalix-ws71.663.499.5199.0893.5±3.0%1184
nalix-tcp-aead84.774.6117.2230.9971.6±5.4%1048
signalr-ws-msgpack93.983.5132.2255.31197.6±4.7%1672
grpc-unary120.8106.7165.1318.91475.8±2.9%1961
grpc-duplex76.869.2105.0180.6925.1±3.4%1256
grpc-unary-tls182.8161.4255.7520.01783.2±1.9%2028
grpc-duplex-tls114.6103.8157.7292.91142.2±1.9%1300
magiconion-unary135.0120.7189.7383.91599.5±4.9%3737
magiconion-hub85.379.4114.9192.21068.4±0.8%1192

Throughput — 32 B payload, N clients closed-loop (ops/s, median of runs)

Library1 client16 clients64 clients
raw-tcp24,865 (±8%)107,698 (±5%)126,234 (±4%)
kestrel-ws19,602 (±3%)91,628 (±2%)106,659 (±0%)
nalix-tcp18,121 (±3%)62,919 (±6%)74,791 (±2%)
nalix-ws16,109 (±2%)64,458 (±5%)69,135 (±5%)
nalix-tcp-aead14,177 (±4%)54,470 (±3%)63,309 (±4%)
signalr-ws-msgpack11,345 (±3%)61,223 (±1%)71,871 (±4%)
grpc-unary8,691 (±4%)48,197 (±6%)58,059 (±5%)
grpc-duplex13,658 (±7%)62,926 (±3%)77,648 (±3%)
grpc-unary-tls6,217 (±13%)35,028 (±2%)43,455 (±4%)
grpc-duplex-tls9,066 (±5%)43,856 (±3%)57,791 (±5%)
magiconion-unary7,658 (±3%)46,330 (±3%)55,721 (±2%)
magiconion-hub12,155 (±3%)46,144 (±13%)67,501 (±2%)

Cost per message at 64 clients — 32 B

Libraryserver alloc/op (B)server CPU/op (µs)client CPU/op (µs)client alloc/op (B)server Gen0/1/2 per run
raw-tcp14313.914.028757/0/0
kestrel-ws017.017.41520/0/0
nalix-tcp5629.319.28871/0/0
nalix-ws19227.122.688840/0/0
nalix-tcp-aead5634.222.98870/0/0
signalr-ws-msgpack67221.824.2155392/0/0
grpc-unary96827.932.8673780/0/0
grpc-duplex26421.922.7232061/0/0
grpc-unary-tls151537.641.5673774/0/0
grpc-duplex-tls81230.429.72322112/0/0
magiconion-unary143228.533.37401117/0/0
magiconion-hub20026.325.1223841/0/0

Throughput — 1024 B payload, N clients closed-loop (ops/s, median of runs)

Library1 client16 clients64 clients
raw-tcp27,356 (±3%)120,792 (±3%)128,443 (±8%)
kestrel-ws19,322 (±5%)88,303 (±2%)107,871 (±3%)
nalix-tcp16,051 (±3%)55,120 (±5%)60,776 (±6%)
nalix-ws13,946 (±3%)52,886 (±5%)61,438 (±3%)
nalix-tcp-aead12,546 (±2%)45,315 (±2%)53,345 (±3%)
signalr-ws-msgpack11,720 (±5%)59,726 (±3%)70,651 (±4%)
grpc-unary8,162 (±1%)45,147 (±5%)56,864 (±5%)
grpc-duplex13,243 (±7%)62,125 (±4%)68,418 (±3%)
grpc-unary-tls5,684 (±5%)31,342 (±3%)43,000 (±4%)
grpc-duplex-tls9,055 (±3%)42,611 (±4%)49,542 (±5%)
magiconion-unary7,661 (±5%)43,613 (±1%)54,752 (±2%)
magiconion-hub11,189 (±1%)51,115 (±4%)58,182 (±1%)

Cost per message at 64 clients — 1024 B

Libraryserver alloc/op (B)server CPU/op (µs)client CPU/op (µs)client alloc/op (B)server Gen0/1/2 per run
raw-tcp14414.413.828754/0/0
kestrel-ws017.117.41520/0/0
nalix-tcp104834.123.21878215/0/0
nalix-ws118430.424.11879232/0/0
nalix-tcp-aead104839.728.11879184/0/0
signalr-ws-msgpack166422.024.12546341/0/0
grpc-unary196028.432.87729164/0/0
grpc-duplex125623.724.03335280/0/0
grpc-unary-tls250438.942.47729150/0/0
grpc-duplex-tls180134.733.73344202/0/0
magiconion-unary373629.934.09707256/0/0
magiconion-hub119230.028.23261201/0/0

Who wins where

ScenarioBest framework (excluding raw baselines)Nalix position
Latency, 32 B, 1 clientNalix TCP (p50 52.2 µs)1st. Nalix WS 55.5 µs, Nalix AEAD 57.6 µs, gRPC duplex 68.4 µs, MagicOnion hub 74.2 µs, SignalR 76.3 µs
Latency, 1 KB, 1 clientNalix TCP (p50 53.6 µs)1st. Nalix WS 63.4 µs, gRPC duplex 69.2 µs, Nalix AEAD 74.6 µs, MagicOnion hub 79.4 µs, SignalR 83.5 µs
Latency, encryptedNalix AEAD at both sizes (57.6 µs vs gRPC duplex TLS 100.4 µs at 32 B; 74.6 µs vs 103.8 µs at 1 KB)1st at both sizes
Throughput, 1 client, 32 BNalix TCP (18.1k)1st (Nalix WS 16.1k, Nalix AEAD 14.2k, gRPC duplex 13.7k)
Throughput, 16 clients, 32 BNalix WS 64.5k / Nalix TCP 62.9k / gRPC duplex 62.9k1st (within run noise of gRPC duplex; SignalR 61.2k)
Throughput, 16 clients, 1 KBgRPC duplex 62.1k3rd (SignalR 59.7k, Nalix TCP 55.1k)
Throughput, 64 clients, 32 BgRPC duplex 77.6k2nd (Nalix TCP 74.8k, SignalR 71.9k, Nalix WS 69.1k)
Throughput, 64 clients, 1 KBSignalR 70.7k3rd–4th (gRPC duplex 68.4k, Nalix WS 61.4k, Nalix TCP 60.8k)
Throughput, encrypted, 64 clientsNalix AEAD (63.3k at 32 B, 53.3k at 1 KB)1st (gRPC duplex TLS 57.8k / 49.5k)
Server allocations/msgNalix TCP / AEAD 56 B at 32 B1st of the frameworks (only raw Kestrel WebSocket, 0 B, is lower)
Server CPU/msg, 64 clients, 32 BSignalR 21.8 µs / gRPC duplex 21.9 µsNalix TCP 29.3 µs, behind both and MagicOnion hub (26.3 µs)

Caveats

  • Loopback and a small shared host. The 4 vCPUs run both the load generator and the server, so high-concurrency numbers are CPU-bound and differences of a few percent are within run-to-run noise (see the ± columns); absolute capacity on bare metal with dedicated network hardware will differ.
  • The client library counts. Latency and throughput include each stack’s client (Nalix SDK, HubConnection, Grpc.Net.Client, MagicOnion’s dynamic client).
  • Closed loop with one request in flight per client. Pipelined or fire-and-forget server push was not measured.
  • Whole-process allocations. The server numbers include background work (timers, task manager), spread across all messages.
  • The encrypted comparison is not like-for-like. TLS 1.3 (AES-GCM via OpenSSL, hardware-accelerated) protects the whole stream, while Nalix encrypts per packet with a managed ChaCha20-Poly1305 after an X25519 handshake, and needs no certificate.