Skip to main content

Coming from StackExchange.Redis

StackExchange.Redis is the established .NET Redis client. Respire covers the same core ground — multiplexed connections, automatic pipelining, Cluster, Sentinel, pub/sub, transactions, and scripting — with a different API shape and some capabilities StackExchange.Redis does not provide. This page compares the two and maps common StackExchange.Redis code to Respire.

Comparisons describe StackExchange.Redis 3.3.1, the version used by Respire's comparison benchmarks. Recheck the StackExchange.Redis claims on this page when that version changes. The upstream configuration and failover/retry documentation describe its RESP3 defaults, smart client handoffs, connection groups, and retry categories.

Pre-release

Respire's public API may still change before a stable release. See the roadmap.

Why switch​

Respire gives you:

Uncached wire performance is comparable. See the benchmarks and stress tests for current measurements against StackExchange.Redis.

Feature comparison​

CapabilityStackExchange.RedisRespire
Target frameworks.NET Framework 4.6.1+, netstandard2.0, .NET 8+.NET 8 and .NET 10
API styleSynchronous and asynchronous methodsAsynchronous only (ValueTask)
Command surfaceFlat methods on IDatabase (HashGet, ListLeftPush)Facets per data type (redis.Hashes.GetStringAsync)
ValuesRedisValue unionTyped results; RespireValue for arguments
Automatic pipeliningYesYes
ProtocolRESP3 attempted by default when HELLO is enabled and the configured server version is 6+; RESP2 fallback and explicit overrideRESP3 preferred by default, with RESP2 fallback
Server-assisted client-side cacheNoYes, opt-in
Blocking commands (BLPOP, BLMOVE, ...)No typed APIs; they would stall the shared connectionTyped APIs on dedicated pooled connections
Pub/subHandlers or ChannelMessageQueueIAsyncEnumerable subscriptions with delivery-gap markers
Sharded pub/sub (Redis 7)YesYes
TransactionsAddCondition checksWATCH-based optimistic concurrency
Distributed locksLockTake, LockExtend, LockReleaseRaw-token APIs plus managed lock handles with keep-alive
Read from replicasCommandFlags.PreferReplica / DemandReplica per commandReadFrom option or WithReadFrom view, with hedged and zone-aware reads
Fire-and-forgetCommandFlags.FireAndForget on any commandExecuteFireAndForgetAsync for raw commands
Multiple databasesGetDatabase(n) per callOne database per client (Database option)
Health-checked failover between deploymentsConnectGroupAsync connection groupsRespireFailoverGroup; read ActiveClient for each operation
Smart client handoffsOpt-in maintenance notifications and endpoint handoff; maintenance notifications are disabled for connection-group members in 3.3.1Opt-in maintenance notifications and handoff; combining handoff with failover groups is tracked in #893
Command retry policyAsync WithRetry(...) wrapper with RetryPolicy and CommandRetry* categoriesConnection recovery and specific routing retries; a general per-command retry policy is tracked in #862
DiagnosticsRegisterProfiler profiling sessionsOpenTelemetry ActivitySource and Meter
IDistributedCacheMicrosoft.Extensions.Caching.StackExchangeRedisRespire.Caching; entries are interchangeable while ValueCodec is unset
Module commandsRaw ExecuteTyped Json, Search, TimeSeries, and Probabilistic packages
Test doubleNone built inRespire.Testing in-memory server

Behavior differences​

These differences can change application behavior after a migration. Review them before you switch.

  • No synchronous API. Every command returns ValueTask or ValueTask<T>. Replace sync calls with await; do not block on .Result.
  • Missing values are null or default. GetStringAsync returns string? and GetAsync<T> returns T?. There is no RedisValue.IsNull check. For a value type, GetAsync<int> returns 0 for a missing key. Use TryGetAsync<T> and check Found when a missing key must differ from a stored default.
  • Server errors throw. A Redis error reply throws RespireServerException; its Code carries the Redis error class.
  • Timeouts are longer by default. CommandTimeout defaults to 10 seconds; StackExchange.Redis defaults to 5 seconds. A timed-out command may still run on the server, as with StackExchange.Redis. asyncTimeout and syncTimeout in a connection string set CommandTimeout.
  • Cancellation is supported. Most methods accept a CancellationToken. Cancellation abandons the wait only; a command that was already written still runs.
  • Batch results throw before the batch runs. Reading RespirePending<T>.Result before ExecuteAsync throws instead of deadlocking.
  • RESP3 changes raw reply shapes. Typed methods hide the difference. Raw RespireResult callers can receive maps, sets, and doubles. Set Protocol = RespProtocol.Resp2 if raw callers need RESP2 shapes. Client-side caching always uses RESP3, so use a separate client without ClientSideCache for those callers. See protocol negotiation.
  • One database per client. Create one client per database index, or use key prefixes. SELECT cannot switch a shared connection.
  • Multiple endpoints need a mode. A comma-delimited string with several endpoints must set cluster=true or serviceName=.... Respire rejects an ambiguous list instead of guessing.
  • Failover groups do not move existing calls. RespireFailoverGroup selects a deployment for new operations only. Read group.ActiveClient for each operation instead of keeping a client reference. In-flight calls on the old deployment are not replayed.
  • Key scans cover the whole deployment. server.KeysAsync scans one server. Keys.ScanAsync scans the client's configured database, and every primary in Cluster mode.
  • Subscriptions report delivery gaps. A subscription stream yields RespireMessageKind.Gap items after a reconnect or buffer overflow. Check message.Kind before reading the payload.

Connecting​

Respire accepts most StackExchange.Redis connection strings, so existing configuration can stay in place:

await using var redis = await RespireClient.ConnectAsync(
"cache-a:6380,password=secret,ssl=true,defaultDatabase=2");

Respire rejects unknown options with ArgumentException instead of ignoring them. Remove StackExchange.Redis-only options such as abortConnect before you pass the string to Respire. See StackExchange.Redis connection strings for the supported options. URIs such as rediss://user:pass@cache-a:6380/2 and RespireOptions also work.

StackExchange.RedisRespire
ConnectionMultiplexer.ConnectAsync(config)RespireClient.ConnectAsync(config)
ConnectionMultiplexer.Connect(config) with abortConnect=falseRespireClient.Create(config) without abortConnect (connects lazily)
multiplexer.GetDatabase()The client itself (IRespireClient)
multiplexer.GetDatabase(2)A separate client with Database = 2
multiplexer.GetSubscriber()redis.SubscribeAsync and redis.PublishAsync on the client
multiplexer.GetServer(endpoint)No per-endpoint selection. redis.Server uses normal routing; *OnAllNodesAsync methods run on every node
ConnectionFailed, ConnectionRestoredConnectionStateChanged
multiplexer.IsConnectedredis.IsConnected
services.AddSingleton<IConnectionMultiplexer>(...)services.AddRespire(...)

Types​

StackExchange.RedisRespire
RedisKeyRespireKey (implicit from string, byte[], ReadOnlyMemory<byte>)
RedisValue (arguments)RespireValue (implicit from strings, bytes, numbers, bool, Guid, and more)
RedisValue (results)string?, byte[]?, long, bool, double, or T?
RedisResultRespireResult — owns pooled memory, so dispose it
RedisChannelRespireChannel
TimeSpan? expiryRespireExpiry (implicit from TimeSpan and DateTimeOffset)
When.NotExists, When.ExistsSetWhen.NotExists, SetWhen.Exists
KeyTimeToLive result TimeSpan?RespireTtl, which separates a missing key from a key without expiry

Commands​

Common string and key commands are on the client. Other commands are grouped into facets: Strings, Keys, Hashes, Lists, Sets, SortedSets, Streams, Bitmaps, HyperLogLog, Geo, VectorSets, Scripts, Functions, Locks, and Server.

// StackExchange.Redis
IDatabase db = multiplexer.GetDatabase();
await db.StringSetAsync("user:1:name", "Ada", TimeSpan.FromMinutes(5), When.NotExists);
string? name = await db.StringGetAsync("user:1:name");
long visits = await db.StringIncrementAsync("visits");
await db.HashSetAsync("user:1", "email", "ada@example.com");
await db.SortedSetAddAsync("leaderboard", "ada", 42);
TimeSpan? ttl = await db.KeyTimeToLiveAsync("user:1:name");
// Respire
await redis.SetAsync("user:1:name", "Ada", TimeSpan.FromMinutes(5), SetWhen.NotExists);
string? name = await redis.GetStringAsync("user:1:name");
long visits = await redis.IncrementAsync("visits");
await redis.Hashes.SetAsync("user:1", "email", "ada@example.com");
await redis.SortedSets.AddAsync("leaderboard", "ada", 42);
RespireTtl ttl = await redis.Keys.ExpiryAsync("user:1:name");
StackExchange.RedisRespire
StringGetAsync, StringSetAsyncGetStringAsync, GetAsync<T>, SetAsync on the client
StringIncrementAsync, StringDecrementAsyncIncrementAsync, DecrementAsync
KeyDeleteAsync, KeyExistsAsync, KeyExpireAsyncDeleteAsync, ExistsAsync, ExpireAsync
HashGetAsync, HashSetAsync, HashGetAllAsyncHashes.GetStringAsync or Hashes.GetBytesAsync, Hashes.SetAsync, Hashes.GetAllAsync or Hashes.GetAllAsync<byte[]>
ListLeftPushAsync, ListRightPopAsyncLists.LeftPushAsync, Lists.RightPopAsync
SetAddAsync, SetMembersAsyncSets.AddAsync, Sets.MembersAsync
SortedSetAddAsync, SortedSetRangeByScoreAsyncSortedSets.AddAsync, SortedSets.RangeByScoreAsync
StreamAddAsync, StreamReadGroupAsyncStreams.AddAsync; Streams.ReadGroupOnceAsync for one batch; single-key Streams.ReadGroupAsync reads continuously
server.KeysAsync(pattern)Keys.ScanAsync(pattern) (IAsyncEnumerable; scans every primary in Cluster mode)
StringGetLeaseAsyncStrings.GetLeaseAsync
ExecuteAsync("CMD", args)ExecuteAsync("CMD", args) or the generated RespireCommands catalog
CommandFlags.FireAndForgetExecuteFireAndForgetAsync

RedisValue results can hold arbitrary bytes. Respire's non-generic read methods return strings, which decode UTF-8 and replace invalid byte sequences. This applies to strings, hashes, lists, sets, sorted sets, and Keys.ScanAsync. For binary data, use GetBytesAsync or the generic <byte[]> overloads, such as GetAsync<byte[]>, Hashes.GetAllAsync<byte[]>, Lists.RangeAsync<byte[]>, Sets.MembersAsync<byte[]>, and SortedSets.RangeAsync<byte[]>. Hash field names, stream field names, and scanned keys are always strings. Use raw HGETALL, XADD, stream read, or SCAN commands when field names or keys are binary. In Cluster mode, a raw SCAN reaches one node only, so it does not enumerate keys on every primary.

StreamReadGroupAsync returns one batch. The single-key Streams.ReadGroupAsync is a continuous consumer: it keeps issuing blocking XREADGROUP calls on a dedicated connection and ends only when its cancellation token is canceled. Use Streams.ReadGroupOnceAsync for a single batch, or the multi-stream Streams.ReadGroupAsync overload for one page across streams. StreamReadOptions supports cumulative Redis 8.10 reply limits, NoAck, and Redis 8.4 ClaimMinIdle.

See strings and keys, collections, and raw commands for the full surface.

Batches and transactions​

StackExchange.Redis batch and transaction methods return tasks that complete when the batch runs. Respire returns RespirePending<T>, and the batch methods drop the Async suffix:

using var batch = redis.CreateBatch();
var name = batch.GetString("user:1:name");
var visits = batch.Increment("visits");
await batch.ExecuteAsync();

Console.WriteLine($"{name.Result}: {visits.Result}");

StackExchange.Redis AddCondition checks have no direct equivalent. Use WATCH through CreateTransactionAsync, read the current state with the client, and retry when CommitAsync returns false:

bool applied;
do
{
await using var watched = await redis.CreateTransactionAsync(["balance"]);
long current = long.Parse(await redis.GetStringAsync("balance") ?? "0");
watched.Set("balance", current - 100);
applied = await watched.CommitAsync();
}
while (!applied);

If the client can read from replicas, read the input through redis.WithReadFrom(RespireReadFrom.Primary). WATCH cannot detect a replica value that was already stale before the watch started.

For single-key compare-and-set on Redis 8.4+, use Strings.SetConditionalAsync or Strings.DeleteConditionalAsync with a RespireValueCondition instead. See batches and transactions and compare values before writing or deleting.

Pub/sub​

StackExchange.Redis delivers messages to a handler or a ChannelMessageQueue. Respire returns a subscription that is an async stream. SubscribeAsync returns after the server acknowledges the subscription:

await using var subscription = await redis.SubscribeAsync("orders", token);

await foreach (var message in subscription.WithCancellation(token))
{
if (message.Kind == RespireMessageKind.Gap)
{
// Messages may have been lost. Reload authoritative state before continuing.
Console.Error.WriteLine($"Delivery gap: {message.Gap}");
continue;
}

Console.WriteLine($"{message.Channel}: {message.Text}");
}

A Gap item means messages may have been lost during a reconnect or buffer overflow; it has no channel or payload. message.Text decodes the payload as UTF-8. Use message.Payload or message.As<byte[]>() for binary payloads. Disposing the subscription unsubscribes. See pub/sub.

Locks, scripts, and key prefixes​

StackExchange.RedisRespire
LockTakeAsync(key, token, expiry)Locks.TryTakeAsync(key, token, expiry)
LockExtendAsyncLocks.ResetExpiryAsync
LockReleaseAsyncLocks.ReleaseAsync
LockQueryAsyncLocks.GetOwnerTokenAsync
—Locks.AcquireAsync(key, expiry, keepAlive: true) for a managed handle that renews itself; keep-alive is opt-in
LuaScript.Prepare(source)RespireScript.Create(source); rewrite @name parameters as KEYS[n] and ARGV[n]
ScriptEvaluateAsync(script, keys, values)Scripts.ExecuteAsync(script, keys, args) (EVALSHA with EVAL fallback)
db.WithKeyPrefix("tenant:")redis.WithKeyPrefix("tenant:")

Respire sends script source unchanged. It does not support StackExchange.Redis named @parameter binding. Rewrite each @key reference as KEYS[n] and each @value reference as ARGV[n], then pass the keys and arguments as arrays in the same order.

See distributed locks and Lua scripting.

Replicas, Cluster, and Sentinel​

StackExchange.RedisRespire
CommandFlags.PreferReplicaReadFrom = RespireReadFrom.ReplicaPreferred, or redis.WithReadFrom(...)
CommandFlags.DemandReplicaRespireReadFrom.Replica
Cluster endpoints in the connection stringcluster=true, or UseCluster = true
serviceName=mymasterserviceName=mymaster, or SentinelPrimaryName

Respire routes only read-only commands to replicas. See read from replicas and Redis Sentinel.

Exceptions​

StackExchange.RedisRespire
RedisConnectionExceptionRespireConnectionException (RespireAuthenticationException for authentication failures)
RedisTimeoutExceptionRespireTimeoutException
RedisServerExceptionRespireServerException
RedisExceptionRespireException

Observability​

Respire has no profiling-session API. It emits OpenTelemetry traces and metrics from an ActivitySource and a Meter, both named Respire:

tracing.AddSource("Respire");
metrics.AddMeter("Respire");

See observability.