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:
| Metric | Why It Matters |
|---|
| Serialization time | CPU cost before Redis write |
| Deserialization time | CPU cost after Redis read |
| Serialized size | Network and Redis memory impact |
| End-to-end latency | Real API performance |
| Allocations | GC pressure |
| Throughput | Maximum sustainable workload |
| Redis memory | Storage efficiency |
| CPU utilization | Infrastructure 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:
| Payload | Approximate Size |
|---|
| Small | 0.5–2 KB |
| Medium | 5–20 KB |
| Large | 50–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:
| Format | Payload |
|---|
| JSON | 18 KB |
| Binary A | 11 KB |
| Binary B | 8 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:
| Serializer | Average | P95 | P99 |
|---|
| A | 2.0 ms | 3.1 ms | 4.0 ms |
| B | 1.8 ms | 5.5 ms | 14.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:
| Format | Payload | Serialize | Deserialize | Allocations | P95 | Memory |
|---|
| JSON | Measure | Measure | Measure | Measure | Measure | Measure |
| Binary A | Measure | Measure | Measure | Measure | Measure | Measure |
| Binary B | Measure | Measure | Measure | Measure | Measure | Measure |
For end-to-end Redis testing:
| Format | GET P95 | SET P95 | P99 | Throughput | Redis Memory |
|---|
| JSON | Measure | Measure | Measure | Measure | Measure |
| Binary A | Measure | Measure | Measure | Measure | Measure |
| Binary B | Measure | Measure | Measure | Measure | Measure |
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.