Skip to main content

A Redis client for .NET. Your calls go out as RESP, the replies come back as string?, long, and your own types.

Get started
dotnet add package Respire --prerelease

On the wire 55 bytes

*5\r\n$3\r\nSET\r\n$8\r\ngreeting\r\n$5\r\nhello\r\n$2\r\nEX\r\n$3\r\n300\r\n

In Respire

await redis.SetAsync("greeting", "hello", expiry: TimeSpan.FromSeconds(300));

Connect, set, get. That is the setup.

One package, one connection string. There is no multiplexer to configure and no value wrapper to unwrap.

Build a small app in five minutes
Program.cs
await using var redis =
await RespireClient.ConnectAsync("redis://localhost");

await redis.SetAsync("user:1", new User("Ada", 36),
expiry: TimeSpan.FromMinutes(5));

User? user = await redis.GetAsync<User>("user:1");
  • Real return typesstring?, long, bool, or your own T?. No value wrapper to unwrap.
  • Reads from memoryServer-assisted client caching. A cached string GET takes about 150 ns and allocates nothing.
  • Pipelined by defaultConcurrent calls share socket writes. There is no batching switch.
  • Blocking commands that workBLPOP and blocking stream reads get their own connections.
  • Coordination built inLocks, rate limiters, semaphores, leader election, queues, and stream workers.
  • Tests without DockerAn in-memory server with a fake clock runs your real client code.

The hard parts, one call each

Locks, rate limits, queues, and retrying transactions are where Redis code usually gets long. In Respire each one is a method on the client you already have.

See every coordination primitive

Memory hit, else Redis, else your factory. Concurrent misses share one factory call.

var redis = await RespireClient.ConnectAsync(new RespireOptions
{
Endpoints = { new RespireEndpoint("localhost") },
ClientSideCache = new() { CoalesceConcurrentMisses = true },
});

User? user = await redis.GetOrSetAsync<User>(
"user:42", LoadUserAsync, TimeSpan.FromMinutes(5));
Read the guide

Replies come back as C# types

Pick a command to see the frame Respire writes, the frame Redis sends back, and the value your code receives. Errors become exceptions. Pushes become cache evictions.

Values and serialization

You write

string? name = await redis.GetStringAsync("user:42:name");

Respire sends

*2\r\n$3\r\nGET\r\n$12\r\nuser:42:name\r\n

Redis replies

$3\r\nAda\r\n

You get

name is "Ada"string?

  • +Simple string
  • -Error
  • :Integer
  • $Bulk string
  • * %Array, map
  • >Push

Concurrent callers share one socket write

Each await looks like its own round trip. Requests that arrive together are written to the socket together, and replies are matched back in order. There is no batching switch to remember.

How the wire layer works
  1. Aawait redis.GetStringAsync("user:1")waiting
  2. Bawait redis.IncrementAsync("visits")waiting
  3. Cawait redis.Hashes.GetAllAsync("cart:9")waiting
Write buffer
ABC
1 write
Redis

Three concurrent awaits

Hot reads never leave the process

Turn on client-side caching and eligible reads stay in bounded process memory. Redis sends a > push frame when a key you read changes, and Respire evicts the stale copy for you.

var options = new RespireOptions
{
Endpoints = { new("localhost") },
ClientSideCache = new(),
};
Read about client-side caching
Server round trip0.0 ns
App
Redis
Local cache hit0.0 ns
AppCache
Redis

Replaying 10,000 times slower than real time

Measured with BenchmarkDotNet on .NET 10: 186.5 μs for a StackExchange.Redis GET against a local server, 151.5 ns and 0 B allocated for a Respire cache hit. See the benchmark run.

Blocking reads wait on their own connections

BLPOP and blocking stream reads hold a connection until data arrives. Respire sends them over a dedicated pool, so a worker waiting 30 seconds for a job never delays the GETs behind it.

string? job = await redis.Lists.LeftPopAsync(
"jobs",
waitFor: TimeSpan.FromSeconds(30));
Build a work queue
Shared connection
Blocking pool
BLPOP jobs waiting, up to 30s

Regular commands keep moving while BLPOP waits.

Try Respire in your next .NET application.

dotnet add package Respire --prerelease