Skip to main content

Benchmarks

Methodology

All benchmarks use an in-memory HttpMessageHandler that returns a fixed pre-serialized response. This isolates library overhead from network I/O.

  • Runtime: .NET 10.0.4, X64 RyuJIT AVX2
  • Platform: Windows 11 (10.0.26200)
  • Tool: BenchmarkDotNet v0.14.0
  • Baseline: RawHttpClient_Get (manual HttpClient + JsonSerializer.DeserializeAsync)

Source: tests/ZeroAlloc.Rest.Benchmarks/


HTTP client: ZeroAlloc.Rest vs Refit vs raw HttpClient

The measured time covers URL building, request construction, body serialization, sending, and response deserialization. Serializer: System.Text.Json (camelCase defaults). Response body: { "id": 1, "name": "Alice" }.

MethodMeanRatioAllocatedAlloc Ratio
RawHttpClient_Get1,253 ns1.001.38 KB1.00
ZeroAlloc_Get2,326 ns2.011.74 KB1.27
Refit_Get9,901 ns8.573.03 KB2.21
RawHttpClient_Post2,585 ns2.241.70 KB1.24
ZeroAlloc_Post4,020 ns3.482.51 KB1.82
Refit_Post9,469 ns8.193.55 KB2.58
RawHttpClient_QueryParam2,005 ns1.731.45 KB1.05
ZeroAlloc_QueryParam2,603 ns2.251.85 KB1.35
Refit_QueryParam13,064 ns11.303.67 KB2.67
RawHttpClient_Delete967 ns0.841.11 KB0.81
ZeroAlloc_Delete1,175 ns1.021.48 KB1.07
Refit_Delete4,943 ns4.282.61 KB1.90

Ratio is relative to RawHttpClient_Get (1,253 ns = 1.00×).

Interpretation

GET / POST: ZeroAlloc.Rest runs at roughly 2–3.5× the raw baseline, covering the generated call frame and serialization dispatch. Refit pays for reflection-based attribute scanning and expression-tree invocation on every call — 8× over baseline.

Query parameters: ZeroAlloc.Rest uses a HeapPooledList<char> rented from ArrayPool<T>.Shared — only 30% slower than the hand-written baseline. Refit rebuilds the URL via reflection on every call, landing at 11× over baseline.

DELETE (void return): No deserialization path; ZeroAlloc.Rest is essentially at parity with raw HttpClient (1.02×). Refit is 4.3×.

Allocations: ZeroAlloc.Rest allocates 1.3–1.8× of raw HttpClient per call. Refit allocates 1.9–2.7×. At 10,000 req/s the difference is ~7–13 MB/s less GC pressure.


Serializer throughput: System.Text.Json vs MemoryPack vs MessagePack

Measured in isolation — serialize or deserialize a single { "id": 42, "name": "Alice" } object. Baseline: Serialize_SystemTextJson.

MethodMeanRatioAllocatedAlloc Ratio
Serialize_SystemTextJson494 ns1.00608 B1.00
Serialize_MemoryPack191 ns0.39464 B0.76
Serialize_MessagePack515 ns1.06416 B0.68
Deserialize_SystemTextJson1,190 ns2.44248 B0.41
Deserialize_MemoryPack485 ns0.99720 B1.18
Deserialize_MessagePack823 ns1.69480 B0.79

Interpretation

MemoryPack is the clear throughput winner: 2.5× faster to serialize and 2.5× faster to deserialize than System.Text.Json. It requires [MemoryPackable] on your DTOs and uses a proprietary binary wire format — not suitable for public HTTP APIs but ideal for internal service-to-service communication.

MessagePack serializes at parity with STJ but deserializes ~1.4× faster, with the lowest allocation footprint on the wire (416 B). Uses the widely-supported MessagePack binary format. Requires [MessagePackObject] / [Key] attributes on DTOs.

System.Text.Json has no DTO annotation requirements and produces human-readable JSON — the right default for public APIs. The STJ deserializer is slower than the binary formats but allocates the least during deserialization (248 B) since it can leverage Utf8JsonReader spans.

Use options.UseSerializer<MemoryPackRestSerializer>() or options.UseSerializer<MessagePackRestSerializer>() to switch serializers in your client registration.


How to reproduce

cd tests/ZeroAlloc.Rest.Benchmarks
dotnet run -c Release

BenchmarkDotNet requires Release mode. Debug builds produce incorrect numbers. Both benchmark classes (RestClientBenchmarks and SerializerBenchmarks) run automatically.