WitRPC offers remarkable flexibility: five transport options and four serialization formats. That's twenty possible combinations, each with different performance characteristics. How do you choose the right one?

Today, we'll demystify these options with real benchmark data, practical guidance, and concrete recommendations for common scenarios.

The Performance Equation

WitRPC performance depends on two key choices:

Total Latency = Transport Time + Serialization Time + Framework Overhead

Transport determines how bytes move between processes:

  • Memory-Mapped Files (MMF): shared memory, no network stack
  • Named Pipes: OS-level IPC, optimized for local communication
  • TCP: raw sockets, full network capability
  • WebSockets: HTTP upgrade, firewall-friendly
  • REST: standard HTTP, maximum compatibility

Serialization determines how objects become bytes:

  • JSON: human-readable, largest size
  • MessagePack: compact binary, good compatibility
  • MemoryPack: zero-copy, fastest for .NET
  • ProtoBuf: Google's format, cross-language support

Let's examine each option with real data.

Transport Deep Dive

Memory-Mapped Files (MMF): The Speed Champion

Memory-mapped files share a region of memory between processes. There's no network stack, no kernel transitions for data transfer; just memory reads and writes.

Configuration:

csharp
// Server
var server = WitServerBuilder.Build(options =>
{
    options.WithService(new MyService());
    options.WithMemoryMappedFile("MyApp-RPC", size: 10_000_000);  // 10 MB buffer
    options.WithMemoryPack();
});

// Client
var client = WitClientBuilder.Build(options =>
{
    options.WithMemoryMappedFile("MyApp-RPC");
    options.WithMemoryPack();
});

Benchmark Results (10 MB payload, 50 iterations):

Serializer One-Way Round-Trip
MemoryPack 15.58 ms 46.70 ms
ProtoBuf 24.08 ms 53.97 ms
JSON 27.48 ms 80.36 ms
MessagePack 35.51 ms 74.55 ms

One detail worth noticing: on large raw-byte payloads, MessagePack lands behind ProtoBuf and even JSON here. MemoryPack's near-zero-copy handling of byte arrays is what puts it firmly in front.

When to use MMF:

  • Desktop app communicating with background service
  • Plugin host ↔ plugin process communication
  • Any scenario where both processes are on the same machine
  • Maximum performance is critical

Limitations:

  • Same machine only
  • Typically one client per MMF channel
  • Requires coordinating buffer size between client and server
  • Memory is reserved even when not in use

Sizing the buffer:

csharp
// Rule of thumb: largest expected message × 2 + overhead
// For 5 MB max messages:
options.WithMemoryMappedFile("MyApp", size: 12_000_000);

Named Pipes: The Reliable Local Choice

Named Pipes are an OS-provided IPC mechanism. They handle synchronization automatically and support multiple clients.

Configuration:

csharp
// Server
var server = WitServerBuilder.Build(options =>
{
    options.WithService(new MyService());
    options.WithNamedPipe("MyApp-Pipe");
    options.WithMessagePack();
});

// Client
var client = WitClientBuilder.Build(options =>
{
    options.WithNamedPipe("MyApp-Pipe");
    options.WithMessagePack();
});

Benchmark Results (10 MB payload, 50 iterations):

Serializer One-Way Round-Trip
MemoryPack 16.66 ms 36.67 ms
ProtoBuf 28.97 ms 55.93 ms
JSON 39.57 ms 90.73 ms
MessagePack 40.00 ms 75.50 ms

When to use Named Pipes:

  • Local IPC with multiple clients
  • Windows services communicating with desktop apps
  • Simpler setup than MMF (no size coordination)
  • Need automatic client management

Limitations:

  • Same machine only (Windows; limited Linux support)
  • Slightly slower than MMF
  • Pipe names are global on the machine (use unique names)

TCP: The Network Workhorse

TCP sockets provide reliable, ordered delivery across networks. WitRPC's TCP transport is optimized for performance.

Configuration:

csharp
// Server
var server = WitServerBuilder.Build(options =>
{
    options.WithService(new MyService());
    options.WithTcp(5000, maxNumberOfClients: 100);
    options.WithMessagePack();
    options.WithEncryption();  // Recommended for network
});

// Client
var client = WitClientBuilder.Build(options =>
{
    options.WithTcp("server.example.com", 5000);
    options.WithMessagePack();
    options.WithEncryption();
});

Benchmark Results:

Local (10 MB payload, 50 iterations):

Serializer One-Way Round-Trip
MemoryPack 23.43 ms 59.16 ms
JSON 33.16 ms 85.89 ms
MessagePack 35.10 ms 77.22 ms
ProtoBuf 36.10 ms 68.38 ms

Remote (1 MB payload, Israel → Germany, medians over 50 runs):

Serializer One-Way Round-Trip
MemoryPack 0.29 s 0.39 s
MessagePack 0.37 s 0.40 s
ProtoBuf 0.37 s 0.25 s
JSON 0.48 s 0.60 s

Numbers are medians over 50 runs; internet timings are noisy, and the ProtoBuf round-trip landing below its own one-way is exactly that session-to-session variance at work.

When to use TCP:

  • Backend microservices communication
  • LAN communication between servers
  • Need raw performance over network
  • Full control over port configuration

Limitations:

  • Requires firewall configuration
  • NAT traversal can be complex
  • Not directly accessible from browsers

Secure TCP:

csharp
// Server with TLS
var certificate = new X509Certificate2("server.pfx", "password");
options.WithTcpSecure(5000, maxNumberOfClients: 100, certificate);

// Client connects to TLS endpoint
options.WithTcpSecure("server.example.com", 5000);

WebSockets: The Firewall-Friendly Option

WebSockets upgrade from HTTP, making them firewall and proxy-friendly. They work in browsers (via Blazor) and behind load balancers.

Configuration:

csharp
// Server
var server = WitServerBuilder.Build(options =>
{
    options.WithService(new MyService());
    options.WithWebSocket("http://0.0.0.0:8080", maxClients: 100);
    options.WithMessagePack();
});

// Client
var client = WitClientBuilder.Build(options =>
{
    options.WithWebSocket("ws://server.example.com:8080");
    options.WithMessagePack();
});

Benchmark Results (10 MB payload, local, 50 iterations):

Serializer One-Way Round-Trip
MemoryPack 48.38 ms 117.55 ms
ProtoBuf 58.32 ms 137.64 ms
MessagePack 67.26 ms 139.08 ms
JSON 71.14 ms 168.77 ms

When to use WebSockets:

  • Blazor WebAssembly clients
  • Communication through corporate firewalls/proxies
  • Load-balanced environments
  • Need to use standard HTTP ports (80/443)

Limitations:

  • Higher overhead than raw TCP
  • HTTP upgrade adds initial latency
  • More complex infrastructure (reverse proxies)

REST: The Compatibility Mode

REST mode provides stateless HTTP endpoints. It's useful for interoperability but doesn't support WitRPC's full feature set.

Configuration:

csharp
// Server
var server = WitServerRestBuilder.Build(options =>
{
    options.WithUrl("http://0.0.0.0:8080/api/");
    options.WithService(new MyService());
    options.WithJson();  // REST typically uses JSON
});

// Client (can be any HTTP client)
var client = WitClientBuilder.Build(options =>
{
    options.WithRest("http://server.example.com:8080");
    options.WithJson();
});

When to use REST:

  • Interoperability with non-.NET clients
  • Debugging/testing with tools like curl or Postman
  • Integration with systems that only support HTTP

Limitations:

  • No event support (server → client push)
  • Request-response only
  • Higher overhead than other transports
  • Stateless (no persistent connection)

Serialization Deep Dive

JSON: The Debuggable Default

JSON is human-readable and universally understood. It's WitRPC's default for a reason: it works everywhere.

csharp
options.WithJson();

Characteristics:

  • ✅ Human-readable (great for debugging)
  • ✅ No special attributes needed on your types
  • ✅ Excellent tooling support
  • ❌ Largest payload size
  • ❌ Slowest serialization

Payload comparison (same object):

JSON:      {"Id":1,"Name":"Task","Status":"Active","Items":[1,2,3]}
           → 58 bytes

Binary:    [compact binary representation]
           → ~25 bytes

Best for:

  • Development and debugging
  • Small payloads where size doesn't matter
  • When you need to inspect traffic

MessagePack: The Balanced Choice

MessagePack is a binary format that's compact and fast, with excellent .NET support.

csharp
options.WithMessagePack();

Characteristics:

  • ✅ ~50% smaller than JSON
  • ✅ ~3x faster than JSON
  • ✅ Works with most .NET types automatically
  • ✅ Good cross-language support
  • ❌ Not human-readable

Best for:

  • Production workloads
  • When you need a balance of speed and compatibility
  • Cross-platform .NET communication

MemoryPack: The Performance King

MemoryPack is a zero-copy serializer designed specifically for .NET. It's the fastest option available.

csharp
options.WithMemoryPack();

Characteristics:

  • ✅ Fastest serialization
  • ✅ Smallest payloads
  • ✅ Zero-copy for many scenarios
  • ❌ Requires [MemoryPackable] attribute on custom types
  • ❌ .NET only (no cross-language support)

Type requirements:

csharp
[MemoryPackable]
public partial class TaskInfo
{
    public int Id { get; set; }
    public string Name { get; set; }
    public TaskStatus Status { get; set; }
}

Best for:

  • Maximum performance
  • Large payload transfers
  • When both sides are .NET

ProtoBuf: The Cross-Language Standard

Protocol Buffers is Google's serialization format, widely used in gRPC and cross-language systems.

csharp
options.WithProtoBuf();

Characteristics:

  • ✅ Excellent cross-language support
  • ✅ Compact binary format
  • ✅ Well-documented schema evolution
  • ❌ Requires [ProtoContract] and [ProtoMember] attributes
  • ❌ Slightly slower than MemoryPack

Type requirements:

csharp
[ProtoContract]
public class TaskInfo
{
    [ProtoMember(1)]
    public int Id { get; set; }
    
    [ProtoMember(2)]
    public string Name { get; set; }
    
    [ProtoMember(3)]
    public TaskStatus Status { get; set; }
}

Best for:

  • When you might need non-.NET clients later
  • Interoperability with gRPC systems
  • Strict schema versioning requirements

Benchmark Summary

Local IPC (10 MB payload)

Transport + Serializer One-Way Round-Trip Relative Speed
MMF + MemoryPack 15.58 ms 46.70 ms 1.0x (fastest)
Named Pipes + MemoryPack 16.66 ms 36.67 ms 1.1x
TCP + MemoryPack 23.43 ms 59.16 ms 1.5x
MMF + JSON 27.48 ms 80.36 ms 1.8x
TCP + JSON 33.16 ms 85.89 ms 2.1x
WebSocket + MemoryPack 48.38 ms 117.55 ms 3.1x
WebSocket + JSON 71.14 ms 168.77 ms 4.6x

Remote (1 MB payload, intercontinental)

Serializer One-Way (median)
MemoryPack 0.29 s
MessagePack 0.37 s
ProtoBuf 0.37 s
JSON 0.48 s

All remote runs went through the same path: the same client machine in Israel, the same Hetzner server in Germany behind an NGINX reverse proxy with SSL termination. The full methodology and cross-framework comparison are in Comparing RPC Frameworks in .NET Applications, and the benchmark suite is open source.

Decision Framework

Quick Reference Table

Scenario Transport Serializer
Desktop app + background service MMF or Named Pipes MemoryPack
Microservices on same network TCP MessagePack
Blazor WebAssembly WebSocket MessagePack
Through corporate firewall WebSocket MessagePack
Development/debugging Any JSON
Maximum performance, same machine MMF MemoryPack
Cross-language potential TCP ProtoBuf
REST API fallback REST JSON

Decision Flowchart

Performance Tuning Tips

1. Match Client and Server Configuration

Both sides must use the same serializer:

csharp
// Server
options.WithMessagePack();

// Client: MUST match!
options.WithMessagePack();

Mismatched serializers cause silent failures or cryptic errors.

2. Size Your MMF Buffer Appropriately

Too small: large messages fail Too large: wastes memory

csharp
// Calculate based on your largest expected message
var maxMessageSize = 5_000_000;  // 5 MB
var bufferSize = maxMessageSize * 2 + 1_000_000;  // Safety margin
options.WithMemoryMappedFile("MyApp", size: bufferSize);

3. Use Binary Serializers for Large Payloads

The difference is dramatic for large data:

1 MB object:
  JSON:       ~1.2 MB on wire, ~45 ms to serialize
  MessagePack: ~0.5 MB on wire, ~12 ms to serialize
  MemoryPack:  ~0.4 MB on wire, ~5 ms to serialize

4. Consider Compression for Network Transfers

For very large payloads over slow networks, compress before sending:

csharp
// Manual compression for large data
public async Task<byte[]> GetLargeDataAsync()
{
    var data = await GetRawDataAsync();
    using var output = new MemoryStream();
    using (var gzip = new GZipStream(output, CompressionLevel.Fastest))
    {
        await gzip.WriteAsync(data);
    }
    return output.ToArray();
}

5. Enable Encryption Only When Needed

Encryption adds overhead. Use it for network communication, skip it for same-machine IPC in trusted environments:

csharp
// Network: enable encryption
options.WithTcp("server", 5000);
options.WithEncryption();

// Same machine, trusted: skip encryption for speed
options.WithNamedPipe("MyApp");
// Don't call WithEncryption()

6. Profile Your Specific Workload

Benchmarks are guidelines, not guarantees. Profile with your actual:

  • Message sizes
  • Message frequency
  • Network conditions
  • Hardware
csharp
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
    await service.ProcessAsync(testData);
}
Console.WriteLine($"1000 calls: {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"Average: {sw.ElapsedMilliseconds / 1000.0} ms/call");

Common Pitfalls

Pitfall 1: Using JSON for Large Binary Data

JSON base64-encodes binary data, increasing size by 33%:

csharp
// Bad: sending images/files over JSON
options.WithJson();
await service.UploadFileAsync(largeByteArray);  // Slow!

// Good: use binary serializer for binary data
options.WithMessagePack();
await service.UploadFileAsync(largeByteArray);  // Fast!

Pitfall 2: WebSockets for Local IPC

WebSockets add unnecessary HTTP overhead for local communication:

csharp
// Wasteful for same-machine communication
options.WithWebSocket("ws://localhost:8080");

// Much faster
options.WithNamedPipe("MyApp");
// or
options.WithMemoryMappedFile("MyApp");

Pitfall 3: Forgetting to Await Async Methods

Unawaited calls can cause connection issues:

csharp
// Bad: fire-and-forget can cause problems
service.ProcessAsync(data);  // Missing await!

// Good: always await
await service.ProcessAsync(data);

Pitfall 4: Not Testing Under Load

A configuration that works for 10 clients may fail at 100:

csharp
// Test with realistic load
var tasks = Enumerable.Range(0, 100)
    .Select(_ => service.ProcessAsync(testData));
await Task.WhenAll(tasks);

Conclusion

WitRPC's flexibility means you can optimize for your specific scenario:

  • Local IPC: MMF + MemoryPack for maximum speed
  • Network services: TCP + MessagePack for the best balance
  • Web clients: WebSocket + MessagePack for compatibility
  • Development: Any transport + JSON for debuggability

The key is understanding the trade-offs:

Dimension Fast Compatible
Transport MMF/Named Pipes WebSocket/REST
Serializer MemoryPack JSON

Start with reasonable defaults (TCP + MessagePack for network, Named Pipes + MessagePack for local), measure your actual performance, and optimize based on real data.

The benchmarks don't lie: the right combination can make your application 4-5x faster than a naive choice. Take the time to choose wisely.


Next up: Building Resilient RPC Clients: Auto-Reconnect and Retry Policies, keeping connections alive on real networks.

This is part 9 of a series on WitRPC. See previous posts: Introducing WitRPC, Getting Started, Real-World Benefits, WitRPC vs. gRPC, Migrating to WitRPC, Under the Hood, Composite Services, and ASP.NET Core Integration.