Redis  

Benchmarking Redis Serialization Formats for High-Throughput .NET APIs

Redis is often introduced into a .NET application as a fast cache, but serialization can become an important part of the performance equation once request volume increases.

Every time an application stores a complex object in Redis, it has to convert that object into bytes. When the value is read back, those bytes must be converted into an object again.

The basic flow looks simple:

.NET Object
    |
    v
Serialization
    |
    v
Bytes
    |
    v
Redis
    |
    v
Bytes
    |
    v
Deserialization
    |
    v
.NET Object

At low traffic, the difference between serialization formats may be difficult to notice.

At high throughput, however, serialization can affect:

  • CPU consumption

  • network payload size

  • Redis memory usage

  • request latency

  • garbage collection

  • allocation rate

  • throughput

This makes serialization an important benchmark target for high-throughput .NET APIs.

The goal of this article is not to declare one format as universally better. The right format depends on the data structure, compatibility requirements, payload size, latency target, and workload.

Why Redis Serialization Matters

Suppose an API stores a customer profile:

public sealed class CustomerProfile
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Country { get; set; } = string.Empty;
}

The application cannot send the .NET object directly to Redis.

It needs a representation such as:

JSON
Binary
MessagePack-style binary representation
Custom byte format

The representation affects both the size of the stored value and the work required to produce and consume it.

For a high-throughput API, this happens repeatedly:

10,000 requests/sec
        |
        +--> Serialize
        |
        +--> Redis operation
        |
        +--> Deserialize

Even a small amount of additional CPU work per request can become significant at scale.

What Should Be Benchmarked?

A meaningful serialization benchmark should measure more than raw serialization speed.

The most useful metrics include:

MetricWhy It Matters
Serialization timeCPU cost before Redis write
Deserialization timeCPU cost after Redis read
Serialized sizeNetwork and Redis memory impact
End-to-end latencyReal API performance
AllocationsGC pressure
ThroughputMaximum sustainable workload
Redis memoryStorage efficiency
CPU utilizationInfrastructure cost

This allows you to distinguish between a format that is fast but produces large payloads and one that is slightly slower but dramatically smaller.

Choose Formats for a Controlled Comparison

A benchmark could compare formats such as:

JSON
Binary serialization
Compact binary serialization

The exact implementations should be selected according to your application's compatibility requirements.

The important rule is to keep the test fair.

Do not compare:

Format A + compression

against:

Format B without compression

unless compression itself is one of the variables being studied.

Likewise, avoid comparing completely different object models.

Use the Same Object Graph

The benchmark should serialize the same logical object.

For example:

public sealed class Order
{
    public Guid Id { get; init; }
    public int CustomerId { get; init; }
    public DateTime CreatedAt { get; init; }
    public decimal Total { get; init; }
    public string Currency { get; init; } = string.Empty;
    public List<OrderItem> Items { get; init; } = [];
}

public sealed class OrderItem
{
    public int ProductId { get; init; }
    public int Quantity { get; init; }
    public decimal Price { get; init; }
}

The same Order instance should be passed through every serializer.

Otherwise, the benchmark may measure differences in the object itself rather than the serialization format.

Test Multiple Payload Sizes

A single payload size is rarely representative.

Use at least three categories:

Small
Medium
Large

For example:

PayloadApproximate Size
Small0.5–2 KB
Medium5–20 KB
Large50–250 KB

These values are benchmark categories, not requirements.

The correct ranges should reflect your application's actual Redis values.

A format that performs well for 1 KB objects may behave differently for 100 KB objects.

Build a Reproducible Benchmark

A benchmark project should isolate serialization from unrelated application behavior.

A useful structure is:

RedisSerializationBenchmark/
    Models/
    Serializers/
    Benchmarks/
    Results/

The benchmark should avoid including:

HTTP middleware
Database queries
Business logic
Logging
Authentication

unless the purpose is an end-to-end API benchmark.

Benchmark Serialization Separately

Start with pure serialization.

Conceptually:

[Benchmark]
public byte[] Serialize()
{
    return Serializer.Serialize(order);
}

Then benchmark deserialization independently:

[Benchmark]
public Order Deserialize()
{
    return Serializer.Deserialize<Order>(payload);
}

This gives you a clean measurement of serializer overhead.

Why Allocation Matters

Two serializers can have similar execution time but very different allocation behavior.

Consider:

Serializer A
2.0 ms
100 KB allocations

Serializer B
2.1 ms
20 KB allocations

At a small workload, the difference may not matter.

At high throughput, it can increase garbage collection activity.

For example:

20,000 operations/sec
×
80 KB additional allocation

creates a large amount of allocation pressure.

The result can be:

More allocations
      |
      v
More GC work
      |
      v
Higher CPU usage
      |
      v
Higher tail latency

This is why allocation metrics belong in the benchmark.

Measure Serialized Payload Size

Payload size is one of the easiest metrics to overlook.

Suppose three formats produce:

FormatPayload
JSON18 KB
Binary A11 KB
Binary B8 KB

The smaller representation can reduce:

Redis memory
Network transfer
Network bandwidth

But smaller does not automatically mean faster.

A format may require more CPU to encode or decode.

That is why size and latency should be evaluated together.

A Simple Size Benchmark

You can measure serialized size directly:

[Benchmark]
public int SerializedSize()
{
    var bytes = Serializer.Serialize(order);

    return bytes.Length;
}

For a complete benchmark, avoid serializing multiple times just to calculate the size if the benchmark is intended to measure one serialization operation.

Capture the payload once during setup when appropriate.

Benchmark Redis Separately From Serialization

After the serializer benchmark, add Redis.

The complete path becomes:

.NET object
     |
     v
Serialize
     |
     v
Redis SET
     |
     v
Redis GET
     |
     v
Deserialize

This gives you end-to-end behavior.

However, do not replace the pure serialization benchmark with the Redis benchmark.

Both answer different questions.

Local Redis Can Produce Misleading Results

A Redis server running on the same machine can make network latency almost irrelevant.

That is useful for measuring serialization overhead, but it does not represent a production deployment where Redis may be:

Separate host
Container
VM
Different availability zone
Different network segment

For a realistic production benchmark, test both:

Local Redis
Remote Redis

The first helps isolate application overhead.

The second helps understand actual network behavior.

Control Redis Configuration

Keep the Redis environment consistent between tests.

Document:

Redis version
Memory configuration
Persistence configuration
Network location
Connection pool settings
TLS configuration

If one benchmark uses TLS and another does not, the comparison becomes less useful.

Reuse Redis Connections

Do not create a new Redis connection for every benchmark operation.

A realistic API generally uses a shared connection manager.

Conceptually:

public sealed class CacheService
{
    private readonly IConnectionMultiplexer _connection;

    public CacheService(IConnectionMultiplexer connection)
    {
        _connection = connection;
    }
}

Connection establishment should be benchmarked separately from steady-state Redis operations.

Otherwise, connection setup can dominate the result.

Measure GET and SET Separately

Redis reads and writes are different operations.

Benchmark:

SET + serialization

and:

GET + deserialization

independently.

For example:

Write path:

Object
  |
  v
Serialize
  |
  v
Redis SET

Read path:

Redis GET
  |
  v
Deserialize
  |
  v
Object

A serializer may perform differently on encode and decode.

Pipeline Workloads Matter

High-throughput APIs frequently issue multiple cache operations.

A realistic workload may look like:

Request
 |
 +--> GET customer
 |
 +--> GET preferences
 |
 +--> GET permissions
 |
 +--> SET response cache

Benchmarking only one Redis command at a time may hide application-level behavior.

After isolated benchmarks, introduce representative multi-operation workloads.

Test Concurrent Requests

Single-threaded benchmarks are useful for understanding serializer behavior but do not represent a busy API.

Test concurrency levels such as:

1
8
32
64
128

The correct levels depend on your target workload.

Track:

Throughput
P50 latency
P95 latency
P99 latency
CPU
Memory
GC
Redis latency

The important question is not just:

How fast is serialization?

It is:

How does the serialization choice behave when the API is under realistic concurrency?

Tail Latency Is Important

Average latency can hide serious problems.

Consider:

SerializerAverageP95P99
A2.0 ms3.1 ms4.0 ms
B1.8 ms5.5 ms14.0 ms

Serializer B has a better average but much worse tail behavior.

For production APIs, P95 and P99 can matter more than a small improvement in average latency.

Use Representative Data

Synthetic data should resemble production data.

For example, an order object should contain realistic variation in:

Number of items
String lengths
Optional properties
Nested objects
Arrays
Null values
Numeric ranges

Avoid benchmarking an unrealistically simple object such as:

new Customer
{
    Id = 1,
    Name = "Test"
};

if production objects contain dozens of properties and nested collections.

Test Schema Evolution

Serialization performance is not the only consideration.

Distributed systems often have different versions of an application running simultaneously.

For example:

Version 1
   |
   v
Redis
   ^
   |
Version 2

The newer application may need to read values written by the older version.

Test scenarios such as:

Old writer -> New reader
New writer -> Old reader

where your format and compatibility requirements make these scenarios relevant.

A slightly faster serializer may not be worth choosing if schema evolution becomes unnecessarily difficult.

Consider Cache Lifetime

Serialization overhead becomes more important when values are frequently written and read.

Suppose a cached object has:

TTL = 1 hour

and is read thousands of times but written only once.

Serialization cost on writes may matter less than deserialization cost on every read.

On the other hand, a response cache that is constantly regenerated may have significant serialization cost on both sides.

Benchmark according to the cache access pattern.

Compression Is a Separate Variable

Compression can reduce payload size further:

Object
  |
  v
Serialize
  |
  v
Compress
  |
  v
Redis

But compression introduces CPU cost.

Do not mix compression into the initial serializer comparison.

First determine:

Serialization performance

Then benchmark:

Serialization + compression

as a separate optimization.

Benchmark Memory Pressure

A high-throughput application can become CPU-bound because of allocations even when Redis itself is fast.

Monitor:

GC collections
Allocated bytes
Gen 0 collections
Gen 1 collections
Gen 2 collections
Working set

The exact metrics depend on the benchmark tooling and runtime.

The goal is to determine whether serialization is creating unnecessary memory pressure.

Example Benchmark Matrix

A useful benchmark report might look like this:

FormatPayloadSerializeDeserializeAllocationsP95Memory
JSONMeasureMeasureMeasureMeasureMeasureMeasure
Binary AMeasureMeasureMeasureMeasureMeasureMeasure
Binary BMeasureMeasureMeasureMeasureMeasureMeasure

For end-to-end Redis testing:

FormatGET P95SET P95P99ThroughputRedis Memory
JSONMeasureMeasureMeasureMeasureMeasure
Binary AMeasureMeasureMeasureMeasureMeasure
Binary BMeasureMeasureMeasureMeasureMeasure

Use actual measurements from your environment rather than filling the table with theoretical numbers.

Example Redis Benchmark Service

A simple service can keep serialization and Redis operations separated:

public sealed class RedisCacheService
{
    private readonly IDatabase _database;
    private readonly ISerializer _serializer;

    public RedisCacheService(
        IDatabase database,
        ISerializer serializer)
    {
        _database = database;
        _serializer = serializer;
    }

    public async Task SetAsync<T>(
        string key,
        T value)
    {
        var payload = _serializer.Serialize(value);

        await _database.StringSetAsync(
            key,
            payload);
    }

    public async Task<T?> GetAsync<T>(
        string key)
    {
        var payload = await _database.StringGetAsync(key);

        if (payload.IsNullOrEmpty)
            return default;

        return _serializer.Deserialize<T>(
            (byte[])payload!);
    }
}

The ISerializer abstraction allows the benchmark to switch implementations without changing the Redis service.

Keep the Benchmark Configuration Explicit

Avoid hidden configuration.

A benchmark configuration might contain:

{
  "payload": "medium-order",
  "concurrency": 32,
  "operations": 10000,
  "redis": "remote",
  "serialization": "format-a",
  "compression": false
}

This makes benchmark runs easier to reproduce.

Common Benchmarking Mistakes

Measuring Only Serializer Speed

Pure serialization benchmarks are useful, but they do not tell you how the serializer behaves inside the API.

Measuring Only End-to-End Redis Latency

This hides the individual serialization cost.

Measure both isolated and integrated workloads.

Using Unrealistic Objects

Small, flat test objects rarely represent production payloads.

Ignoring Allocations

A serializer can look fast while creating substantial GC pressure.

Using One Concurrency Level

Serialization behavior can change under load.

Ignoring P99

Average latency can hide tail-latency problems.

Recreating Redis Connections

Connection setup can dominate benchmark results.

Using Local Redis Only

Local benchmarks are useful for isolation but do not represent network latency.

Mixing Compression With Serialization

Compression should be treated as a separate benchmark dimension.

Benchmarking With Logging Enabled

Verbose logging can distort high-throughput measurements.

How to Interpret Results

Imagine the benchmark produces:

Format A:
Small payload
Excellent latency
Large serialized size

Format B:
Medium latency
Small serialized size
Low allocations

Format C:
Fastest serialization
Higher memory usage
Poor compatibility

There is no automatic winner.

If Redis memory is expensive, Format B may be attractive.

If CPU is the primary bottleneck, Format C might be useful.

If interoperability is important, Format A could still be the right choice.

Benchmarking should support an engineering decision, not replace one.

Recommended Benchmark Strategy

For a production .NET API, use this progression:

1. Benchmark pure serialization
2. Benchmark pure deserialization
3. Measure payload size
4. Measure allocations
5. Add Redis GET/SET
6. Test realistic concurrency
7. Measure P95/P99
8. Test representative payloads
9. Test remote Redis
10. Validate schema compatibility

This provides enough information to understand where the actual bottleneck exists.

Best Practices

Use the same object graph across formats.

Keep benchmark configuration version controlled.

Run multiple iterations.

Separate cold-start measurements from steady-state measurements.

Measure both serialization and deserialization.

Track allocations and garbage collection.

Test realistic payload sizes.

Include concurrency.

Measure P95 and P99 latency.

Keep compression as a separate experiment.

Test schema evolution before selecting a format for long-lived cached data.

Most importantly, benchmark the complete application path after understanding the isolated serializer results.

Frequently Asked Questions

Is JSON too slow for high-throughput Redis APIs?

Not necessarily. JSON can be perfectly adequate for many workloads. The correct decision depends on payload size, throughput, latency requirements, compatibility needs, and available CPU.

Does a smaller Redis value always improve performance?

No. Smaller values reduce network and memory requirements, but serialization and deserialization may require additional CPU.

Should serialization happen on the application thread?

It depends on the workload. For normal-sized objects, synchronous CPU work may be appropriate. Extremely large payloads or specialized workloads may require a different design.

Should I optimize serialization before Redis latency?

Measure first. If Redis network latency dominates the request, a small serialization improvement may have little practical effect.

Is binary serialization always better?

No. Binary formats can provide smaller payloads or lower CPU cost in some workloads, but compatibility, debugging, schema evolution, and operational requirements also matter.

How many Redis operations should a benchmark run?

Use enough operations to produce stable measurements. A benchmark with thousands of operations is generally more informative than a handful of requests, but the exact workload should reflect expected production traffic.

Conclusion

Serialization is easy to overlook when designing a Redis-backed .NET API because Redis itself is extremely fast. Once an application reaches high throughput, however, the work surrounding the Redis operation can become significant. Serialization, deserialization, memory allocation, payload size, and network transfer all contribute to the actual request cost.

The best way to evaluate serialization formats is to benchmark them systematically. Start with isolated serialization and deserialization measurements, then add Redis operations, realistic payloads, concurrency, and production-like network conditions. Track latency, throughput, allocations, memory usage, and payload size instead of relying on a single speed number.

There is no universally optimal serialization format for every .NET API. The right choice is the one that provides an acceptable balance of performance, memory efficiency, compatibility, operational simplicity, and reliability for your specific Redis workload.