Skip to main content

Coming from StackExchange.Redis

This page compares Respire with StackExchange.Redis and maps common StackExchange.Redis code to Respire. Both cover multiplexed connections, automatic pipelining, Cluster, Sentinel, pub/sub, transactions, and scripting. Respire has a different API shape and some extra capabilities.

The opt-in migration analyzer and code fix converts supported IDatabase methods to native Respire calls and flags other calls for manual review.

To migrate gradually, use the incremental migration adapter. It accepts RedisKey, RedisValue, and RedisResult, and provides a limited IConnectionMultiplexer for the official distributed cache, DataProtection, and Hangfire. It is not a general StackExchange.Redis replacement. It does not support Microsoft's SignalR backplane; use Respire.SignalR.

For applications using RedLock.net or madelson's DistributedLock.Redis, see migrating Redis coordination libraries for API mappings, renewal and timeout handling, and the differences between quorum leases and fenced locks.

Verified on 14 October 2026 against StackExchange.Redis 3.4.5 (latest stable release) and 4.0.127-alpha (latest published 4.0 preview). The 4.0 column describes that preview, not a stable 4.0 release or a promise about its final API. Earlier 3.x releases may lack features added in 3.4. The published benchmarks used 3.3.1 and do not measure 4.0.

See the roadmap for supported features and the experimental API policy for compatibility limits.

Why switch​

Respire gives you:

See the benchmarks and stress tests for measurements against StackExchange.Redis. The published 3.3.1 results do not establish performance against 4.0.

Feature comparison​

CapabilityStackExchange.Redis 3.x (3.4.5)StackExchange.Redis 4.0.127-alphaRespire
Target frameworks.NET Framework 4.6.1/4.7.2, netstandard2.0, .NET 8/10Same targets.NET 8 and .NET 10
API styleSync methods and async Task methodsNew command groups return ValueTask; legacy sync/Task methods remain supportedAsynchronous only (ValueTask)
Command surfaceFlat methods on IDatabase (StringGetAsync)Groups on the database (db.Strings.GetAsync), alongside the flat APIFacets per data type (redis.Strings.GetStringAsync)
ValuesRedisValue unionRedisValue for ordinary string results; groups also expose pooled ReadOnlyLease<T> resultsTyped results; RespireValue for arguments and pooled result APIs
Per-call cancellationNo token on ordinary IDatabaseAsync commandsCommand groups accept CancellationTokenMost methods accept CancellationToken
Automatic pipeliningYesYesYes
ProtocolRESP3 attempted when HELLO is enabled and configured server version is 6+; RESP2 fallback/overrideSame negotiation; client-side caching requires RESP3RESP3 preferred, with RESP2 fallback
Server-assisted client-side cacheNo built-in cacheOpt-in ClientCache; covers both legacy calls and groupsOpt-in ClientSideCache
Blocking Redis commands (BLPOP, BLMOVE, ...)No typed APIsNo typed APIs in this previewTyped APIs on dedicated pooled connections
Pub/subHandlers or ChannelMessageQueue; separate pub/sub connection by default under RESP3 since 3.4Same subscription API and connection defaultIAsyncEnumerable subscriptions with delivery-gap markers
Sharded pub/sub (Redis 7)YesYesYes
TransactionsAddCondition checksSame checks; batches and transactions also expose groupsWATCH-based optimistic concurrency
Distributed locksLockTake, LockExtend, LockReleaseSame helpers on the legacy IDatabase surfaceRaw-token APIs plus managed lock handles with keep-alive
Read from replicasCommandFlags.PreferReplica / DemandReplicaSame per-command flagsReadFrom or WithReadFrom, with hedged and zone-aware reads
Fire-and-forgetCommandFlags.FireAndForgetSame flag, including on groupsExecuteFireAndForgetAsync for raw commands
Multiple databasesGetDatabase(n)SameOne database per client (Database option)
Health-checked failover between deploymentsConnectGroupAsync connection groupsSame, with group configuration in MultiGroupOptionsRespireFailoverGroup; read ActiveClient for each operation
Smart client handoffsMaintenance notifications and endpoint handoff; automatic for Redis Enterprise and Azure Managed Redis since 3.4; disabled inside connection groupsSame availability and group restrictionOpt-in maintenance handoff, including failover group candidates
Command retry policyAsync WithRetry(...) wrapper with RetryPolicy and CommandRetry* categoriesWithRetry(...) also exposes groups; configurable connection/group policyRetryPolicy with category limits, plus whole-unit batch and transaction retries
DiagnosticsRegisterProfiler profiling sessionsSame profiling APIOpenTelemetry ActivitySource and Meter
AnalyzersTransaction and sync-over-async checks (SER305–SER308)Those checks remain availablePooled-result and batch-read checks (RESP001, RESP002); source-generator checks (RESP003–RESP006); sync-over-async (RESP009); client lifetime, unbounded reads, and blocking subscription consumers (RESP010–RESP012)
IDistributedCacheMicrosoft.Extensions.Caching.StackExchangeRedisLegacy IConnectionMultiplexer / IDatabase API remains available for adaptersRespire.Caching; entries interchangeable while ValueCodec is unset
Module/custom commandsRaw ExecuteRaw commands plus extensible groups and Context.SendAsync<T>Typed Json, Search, TimeSeries, and Probabilistic packages
In-memory test serverNone shipped in the client packageNone shipped in the client packageRespire.Testing

The 3.x entries use the tagged configuration, failover/retry, maintenance, and sync/async documentation. For 4.0, see the preview's command groups, legacy API mapping, client-side cache, protocol, and failover/retry documentation. These links are pinned to the compared versions.

Both compared StackExchange.Redis versions also offer opt-in DedicatedThreads to keep Redis traffic off the global thread pool; see the 3.x and 4.0 threading guidance.

What changes when coming from 4.0​

Async command groups, ValueTask, cancellation, and client-side caching are also available in the 4.0 preview. Compare their behavior and configuration before choosing a client. Its legacy IDatabase methods are still supported; upgrading StackExchange.Redis does not require a migration to Respire or a rewrite to groups.

Similar method names do not imply identical result types: StackExchange.Redis db.Strings.GetAsync(key) returns RedisValue; Respire's redis.Strings.GetStringAsync(key) returns string?, while GetAsync<T> uses the selected codec. Both offer pooled results that must be disposed. Review binary values, missing values, and ownership when porting callers.

StackExchange.Redis 4.0's Context.Blocking() blocks the calling thread; it does not add typed Redis blocking queue commands such as BLPOP. A command group's cancellation token prevents a command from being sent if it has not yet been written to the connection. Once written, the command still runs on Redis; cancellation ends the caller's wait and discards the reply.

The examples below map the flat 3.x API, which is also available in 4.0. For callers already using 4.0 groups, use the upstream mapping to identify the Redis operation, then check the Respire result and options rather than applying a mechanical rename.

Behavior differences​

Review these before you switch; they can change application behavior.

  • 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. See timeouts and thread-pool starvation for timeout stages, diagnosis, and a mapping of StackExchange.Redis v3 message fields.
  • Cancellation is supported. Most methods accept a CancellationToken. Cancellation abandons the wait only; a command that was already written still runs.
  • Primary reconnect waits are bounded. After initial connection, definitely-unsent standalone and Sentinel primary commands wait in a FIFO backlog by default, including raw and fire-and-forget commands, whole batches and unwatched transactions. BacklogCapacity defaults to 1024 waiting operations; each keeps its original timeout and cancellation. Set BacklogPolicy = RespireBacklogPolicy.FailFast to opt out. This does not replay accepted commands and does not require RetryPolicy. Sentinel waiting survives generation retirement and selects a ROLE-validated current primary. Cluster commands use independent slot queues, reselecting the current owner and read policy before sending; a batch slot group or unwatched transaction counts as one entry. Standalone and Sentinel replica reads use a separate FIFO that reselects under the original read policy and current membership, including read-only procedures, streams and hedged reads. See connection recovery for exclusions and the distinction between backlog, reconnect and command retry policies.
  • 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 = RespireProtocol.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:

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

Azure portal strings including abortConnect=False work as pasted. Unsupported options are ignored with one warning per key per client when LoggerFactory is configured. Invalid mapped values throw RespireConfigurationException. See StackExchange.Redis connection strings for mappings and explicit rejections. 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) (connects lazily; accepts abortConnect=false)
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)redis.Server.OnNode(endpoint) for one explicit node. redis.Server otherwise 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 — GC-owned by default; dispose results from explicit lease APIs
RedisChannelRespireChannel
TimeSpan? expiryRespireExpiry (implicit from TimeSpan and DateTimeOffset)
When.NotExists, When.ExistsRespireSetWhen.NotExists, RespireSetWhen.Exists
KeyTimeToLive result TimeSpan?RespireTtlResult, 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), RespireSetWhen.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);
RespireTtlResult 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

Binary data. Non-generic read methods return UTF-8 strings and replace invalid bytes. This applies to strings, hashes, lists, sets, sorted sets, and Keys.ScanAsync. For binary data, use GetBytesAsync or a <byte[]> overload, such as GetAsync<byte[]>, Hashes.GetAllAsync<byte[]>, Lists.RangeAsync<byte[]>, Sets.MembersAsync<byte[]>, or 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 they are binary. In Cluster mode, a raw SCAN reaches one node only.

Stream consumer groups. StackExchange.Redis StreamReadGroupAsync returns one batch; the equivalent is Streams.ReadGroupOnceAsync. The single-key Streams.ReadGroupAsync reads continuously on a dedicated connection until its cancellation token is cancelled. The multi-stream Streams.ReadGroupAsync overload returns one page across streams. RespireStreamReadOptions supports 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​

Respire batch methods return RespirePending<T> instead of tasks, and 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}");

AddCondition has no direct equivalent. Watch keys with CreateTransactionAsync, read the current state, 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​

A Respire subscription is an async stream, not a handler or ChannelMessageQueue. SubscribeAsync returns after the server acknowledges the subscription:

await using var subscription = await redis.SubscribeAsync("orders", cancellationToken: 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 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 and has no named @parameter binding. Rewrite each @key as KEYS[n] and each @value as ARGV[n], then pass keys and arguments as arrays in that 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.