Redis  

Benchmarking Redis Connection Multiplexing for High-Throughput .NET APIs

Introduction

When a .NET API starts handling a large number of concurrent requests, database performance is not always the first bottleneck.

Redis connection management can become an important part of the latency and throughput equation.

A common assumption is that creating more Redis connections will automatically improve performance. In practice, too many connections can increase resource consumption, connection management overhead, contention, and operational complexity.

Connection multiplexing takes a different approach: multiple logical operations share a smaller number of physical connections.

For high-throughput ASP.NET Core APIs, the important question is not simply whether multiplexing works. The useful engineering question is:

How does Redis connection multiplexing affect throughput, latency, CPU, memory, and connection overhead under realistic .NET workloads?

This article explains how to design a repeatable benchmark and what to measure before choosing a connection strategy.

What Is Redis Connection Multiplexing?

Without multiplexing, an application may establish separate connections for individual operations or request flows:

API Request 1 -> Redis Connection 1
API Request 2 -> Redis Connection 2
API Request 3 -> Redis Connection 3
API Request 4 -> Redis Connection 4

With multiplexing:

API Request 1 ----\
API Request 2 -----\
API Request 3 ------> Shared Redis Connection
API Request 4 -----/
API Request 5 ----/

Multiple operations can share the same underlying connection infrastructure.

This is particularly useful when an application processes many short Redis commands.

The goal is to reduce unnecessary connection overhead while maintaining sufficient concurrency.

Why Connection Count Matters

Every Redis connection has some cost.

Depending on the environment, connections consume:

  • Client-side memory

  • Redis-side resources

  • Socket resources

  • TLS resources when encryption is enabled

  • Connection management overhead

  • Network resources

Consider an API receiving:

10,000 requests/second

If every request creates or manages its own Redis connection, the application can spend significant resources managing connections instead of processing useful work.

A better architecture is usually:

ASP.NET Core
      |
      v
Redis Client Layer
      |
      v
Connection Multiplexing
      |
      v
Redis

Multiplexing vs Connection Pooling

These concepts are related but not identical.

Connection Pooling

A pool maintains multiple physical connections and leases them to operations.

Request
  |
  v
Connection Pool
  |
  +--> Connection 1
  +--> Connection 2
  +--> Connection 3
  +--> Connection 4

Multiplexing

Multiple operations can share the same physical connection.

Request 1 --\
Request 2 ---\
Request 3 ----> Multiplexed Connection
Request 4 ---/

The correct approach depends on the Redis client, workload, protocol behavior, and concurrency characteristics.

The benchmark should therefore measure the actual implementation rather than assuming one architecture is always superior.

What Should Be Benchmarked?

A useful benchmark should measure more than average latency.

Track at least:

MetricWhy It Matters
Requests/secOverall throughput
P50 latencyTypical request experience
P95 latencyTail behavior
P99 latencyWorst common cases
Redis operations/secCache workload
Connection countResource utilization
CPUClient/server overhead
MemoryConnection and buffering cost
ErrorsReliability
TimeoutsSaturation indicator

For high-throughput APIs, P95 and P99 are especially important.

Build a Representative Workload

A benchmark should resemble real application behavior.

For example:

GET customer profile
GET cached configuration
SET session data
GET product information
INCR request counter

Avoid benchmarking only:

SET key value
GET key

if the real application performs more complex operations.

The benchmark should represent:

  • Command mix

  • Key distribution

  • Payload sizes

  • Concurrency

  • Request frequency

  • Cache hit patterns

Example ASP.NET Core Redis Usage

A typical application may inject a Redis client abstraction into its services.

For example:

public sealed class ProductCache
{
    private readonly IConnectionMultiplexer _redis;

    public ProductCache(IConnectionMultiplexer redis)
    {
        _redis = redis;
    }

    public async Task<string?> GetAsync(string key)
    {
        var database = _redis.GetDatabase();

        return await database.StringGetAsync(key);
    }
}

The important point is that the connection infrastructure should normally have an appropriate application lifetime rather than being recreated for every request.

Do Not Create Connections Per Request

This pattern is problematic:

public async Task<string?> GetValueAsync(string key)
{
    using var redis = await CreateConnectionAsync();

    var database = redis.GetDatabase();

    return await database.StringGetAsync(key);
}

At low traffic, the problem might not be obvious.

Under heavy concurrency, however, repeated connection creation can produce:

Request
  |
  +--> Connection Setup
  +--> Redis Operation
  +--> Connection Cleanup

instead of:

Request
  |
  +--> Redis Operation

The benchmark should make this overhead visible.

Establish the Baseline

Before testing different multiplexing configurations, establish a baseline.

Record:

Redis Version
Client Library Version
.NET Version
CPU
Memory
Network
Payload Size
Concurrency
Command Mix

Keep these variables consistent between benchmark runs.

The only major variable should be the connection strategy.

Test Different Concurrency Levels

A single concurrency level can produce misleading results.

Test multiple levels such as:

10 concurrent requests
50 concurrent requests
100 concurrent requests
500 concurrent requests
1,000 concurrent requests

The exact values should reflect the expected production workload.

A strategy that performs well at 50 concurrent operations may behave differently at 1,000.

Test Different Connection Counts

If the client supports configurable connection strategies, benchmark multiple configurations.

For example:

1 physical connection
2 physical connections
4 physical connections
8 physical connections
16 physical connections

The objective is not to find the largest possible number.

Instead, identify where additional connections stop producing meaningful gains.

A typical result might look like:

ConnectionsThroughputP95P99CPU
172K ops/s4.2 ms7.8 ms38%
291K ops/s3.4 ms5.9 ms41%
4104K ops/s3.0 ms5.1 ms45%
8105K ops/s3.0 ms5.2 ms51%
16104K ops/s3.2 ms5.8 ms58%

These numbers are illustrative rather than universal.

The important pattern is the point of diminishing returns.

Find the Saturation Point

Suppose throughput improves from:

1 connection -> 2 connections

and again from:

2 connections -> 4 connections

but barely changes from:

4 connections -> 8 connections

The system may already have reached a practical saturation point.

Adding more connections could increase CPU and memory without improving application throughput.

This is why benchmark results should be evaluated as a curve rather than a single winner.

Measure P50, P95, and P99

Consider two configurations:

Configuration A
P50: 2 ms
P95: 4 ms
P99: 7 ms

Configuration B
P50: 2 ms
P95: 5 ms
P99: 25 ms

Average latency might make both configurations look similar.

P99 reveals that Configuration B has a much worse tail.

For APIs, tail latency can be especially important because a slow cache operation can contribute to an already slow request.

Test Payload Sizes

Redis performance can change significantly as payload size increases.

Test multiple payload categories:

Small: 100 bytes
Medium: 1 KB
Large: 10 KB
Very Large: 100 KB

Again, the exact values should represent the application.

A configuration optimized for small cache values may behave differently when transferring larger payloads.

Test Cache Hit and Miss Behavior

A cache benchmark should distinguish between:

Cache Hit

and:

Cache Miss

For example:

var value = await database.StringGetAsync(key);

if (value.IsNullOrEmpty)
{
    // Load from primary data source
}

If the benchmark measures only successful Redis reads, it may underestimate the real workload.

A production-like benchmark should model the expected hit ratio.

Test Key Distribution

Key distribution also matters.

A benchmark where every request accesses a unique key is different from one where thousands of requests repeatedly access the same small set of hot keys.

Consider:

Uniform Distribution

versus:

Hot-Key Distribution

A hot-key workload can expose contention and application-level bottlenecks that a uniform workload may hide.

Benchmark Parallel Operations

High-throughput applications frequently execute independent cache operations concurrently.

For example:

var productTask = database.StringGetAsync("product:100");
var inventoryTask = database.StringGetAsync("inventory:100");
var pricingTask = database.StringGetAsync("pricing:100");

await Task.WhenAll(productTask, inventoryTask, pricingTask);

This is a useful workload for evaluating multiplexing because multiple logical operations are in flight at the same time.

Sequential vs Concurrent Workloads

Benchmark both.

Sequential

var product = await database.StringGetAsync("product");
var price = await database.StringGetAsync("price");

Concurrent

var productTask = database.StringGetAsync("product");
var priceTask = database.StringGetAsync("price");

await Task.WhenAll(productTask, priceTask);

The concurrent workload is often more representative of high-throughput application behavior.

Build a Simple Benchmark Harness

A simple benchmark can collect operation latency.

public sealed record RedisBenchmarkResult(
    int Concurrency,
    int ConnectionCount,
    double P50Ms,
    double P95Ms,
    double P99Ms,
    double OperationsPerSecond,
    long Errors);

A benchmark runner can execute the same workload against different configurations and produce comparable records.

For more rigorous microbenchmarking, use a dedicated benchmarking framework and separate application-level load testing from low-level method benchmarks.

Measure End-to-End API Latency

Redis operations should not be evaluated only in isolation.

Consider an API request:

HTTP Request
    |
    v
ASP.NET Core
    |
    v
Authentication
    |
    v
Redis
    |
    v
Business Logic
    |
    v
HTTP Response

A Redis optimization that saves 0.2 ms may not matter if the endpoint spends 100 ms elsewhere.

Conversely, a Redis tail-latency problem can become important when Redis is on the critical path of thousands of requests.

Monitor Connection Count

During the benchmark, record the actual number of physical connections.

You want to verify that the configuration behaves as expected.

For example:

Logical Operations: 100,000
Physical Connections: 4

is fundamentally different from:

Logical Operations: 100,000
Physical Connections: 100,000

The purpose of multiplexing is to efficiently handle many logical operations without requiring a corresponding increase in physical connections.

Measure Client CPU

Connection multiplexing can reduce connection-management overhead, but the client still has work to perform.

Measure:

Process CPU
GC Activity
Thread Pool Usage
Allocated Memory

A configuration that produces slightly higher throughput but significantly increases CPU may not be the best production choice.

Watch for Thread Pool Pressure

High-throughput .NET applications can experience ThreadPool pressure when asynchronous workloads are incorrectly implemented or blocked.

Avoid patterns such as:

var result = database.StringGetAsync(key).Result;

Prefer asynchronous execution:

var result = await database.StringGetAsync(key);

A Redis benchmark should therefore test the actual asynchronous application path rather than artificially introducing blocking.

Benchmark Timeouts and Failures

Performance testing should include failure metrics.

Track:

Timeouts
Connection Failures
Command Failures
Retries
Cancelled Requests

A configuration that achieves high throughput but produces frequent timeouts is not a successful configuration.

Test Under CPU Pressure

A useful benchmark has multiple operating conditions.

For example:

Normal CPU
Moderate CPU Pressure
High CPU Pressure

This can reveal whether the Redis client remains stable when the API process is already under load.

Test Redis Server Pressure Separately

Client-side connection multiplexing is only one part of the system.

Monitor Redis itself:

CPU
Memory
Network
Commands/sec
Latency
Connected Clients
Evictions
Errors

A client optimization cannot compensate for a server that is already saturated.

Test Connection Failure and Recovery

A production benchmark should include failure scenarios.

For example:

Normal
   |
   v
Redis Connection Failure
   |
   v
Recovery
   |
   v
Normal Traffic

Measure:

  • Error rate

  • Recovery time

  • Request latency during recovery

  • Reconnection behavior

  • Connection count after recovery

This is especially important for long-running ASP.NET Core services.

Avoid Benchmark Warm-Up Bias

The first operations can include initialization overhead.

For example:

Application Startup
     |
     v
Connection Initialization
     |
     v
Warm-Up
     |
     v
Measurement

Do not mix startup behavior with steady-state measurements unless startup latency is specifically what you want to measure.

Use Multiple Benchmark Runs

One run is not enough.

A better process is:

Warm-up
Run 1
Run 2
Run 3
Run 4
Run 5

Then compare the distributions.

This reduces the chance that a temporary CPU spike, network fluctuation, or unrelated system event determines the conclusion.

Identify the Knee of the Curve

One of the most useful outcomes of connection benchmarking is identifying the point where additional connections stop providing meaningful benefit.

Conceptually:

Throughput
   ^
   |             _________
   |          __/
   |       __/
   |    __/
   |___/
   +--------------------------> Connections
             ^
        Practical Range

The ideal configuration is often near this point rather than at the maximum connection count.

Example Comparison Model

A benchmark result can be represented as:

public sealed record ConnectionBenchmark(
    int ConnectionCount,
    int Concurrency,
    double Throughput,
    double P50,
    double P95,
    double P99,
    double CpuPercent,
    long AllocatedBytes,
    long Errors);

Then the benchmark report can compare configurations using both performance and resource efficiency.

Example Decision Rule

A simple policy might prioritize:

1. No significant errors
2. Acceptable P99 latency
3. Required throughput
4. Reasonable CPU
5. Reasonable memory

For example:

static bool IsAcceptable(ConnectionBenchmark result)
{
    return result.Errors == 0
        && result.P99 < 20
        && result.Throughput >= 100_000;
}

The threshold values are examples. Production thresholds should come from the application's SLA and capacity requirements.

Common Mistakes

Creating Redis Connections Per Request

This adds unnecessary connection management overhead.

Assuming More Connections Are Always Faster

More connections can eventually increase resource consumption without improving throughput.

Measuring Only Redis

End-to-end API behavior matters.

Using Only Average Latency

P95 and P99 can expose tail-latency problems.

Testing Only One Payload Size

Network and serialization costs change with payload size.

Ignoring Cache Misses

Real applications often have both hits and misses.

Ignoring Hot Keys

Uniform random workloads may not represent production traffic.

Benchmarking Only One Concurrency Level

The optimal configuration can change significantly as concurrency increases.

Ignoring Failure Recovery

A configuration that performs well during normal traffic may behave poorly during connection failures.

Best Practices

  1. Reuse appropriate long-lived Redis client infrastructure.

  2. Benchmark multiplexing under realistic concurrency.

  3. Test multiple physical connection configurations.

  4. Measure throughput alongside P50, P95, and P99 latency.

  5. Include realistic command mixes.

  6. Test multiple payload sizes.

  7. Include cache hits and misses.

  8. Test realistic key distributions.

  9. Measure client CPU and memory.

  10. Monitor Redis server resource usage.

  11. Test failure and reconnection behavior.

  12. Warm up the application before measurement.

  13. Run multiple benchmark iterations.

  14. Identify the point of diminishing returns.

  15. Select the smallest configuration that reliably meets the application's performance requirements.

Frequently Asked Questions

Is one Redis connection always enough?

Not necessarily. The appropriate configuration depends on concurrency, workload, client implementation, network conditions, and application requirements.

Does connection multiplexing eliminate the need for connection management?

No. It reduces the need for large numbers of independent physical connections, but connection lifecycle, failure recovery, timeouts, and resource limits still need to be managed.

Should I increase Redis connections when latency increases?

Not automatically. First determine whether the bottleneck is Redis, the client, CPU, network, serialization, ThreadPool pressure, or another component.

Should Redis benchmarks use production data?

Use a representative dataset, but handle sensitive production information appropriately. A sanitized production-like dataset is often preferable.

Is throughput more important than latency?

Neither metric is universally more important. High-throughput systems still need predictable tail latency, while latency-sensitive APIs may prioritize P95/P99 response times over maximum throughput.

How do I choose the final connection configuration?

Choose the configuration that meets your required throughput and latency targets with acceptable CPU, memory, error rates, and operational complexity. Do not optimize for connection count alone.

Conclusion

Redis connection multiplexing can be an important optimization for high-throughput .NET APIs, but the correct configuration should be determined through measurement rather than assumptions.

A meaningful benchmark should vary concurrency and connection count while measuring throughput, P50/P95/P99 latency, CPU, memory, errors, payload sizes, cache behavior, and failure recovery.

The most useful result is usually not “more connections are faster.” It is identifying the smallest and most stable connection configuration that satisfies the application's workload and latency requirements.

For production systems, that combination of throughput, predictable tail latency, and efficient resource usage is far more valuable than maximizing Redis connections.