You've been running Redis for years. It handles session state, output caching, pub/sub, and half a dozen other concerns your application depends on. Then the licence changed, your team started asking questions, and suddenly "just use Redis" wasn't the default answer anymore. If you've been quietly evaluating alternatives, there's one you should look at seriously — and it's written in C#.
Garnet is an open-source remote cache store from Microsoft Research. It speaks the RESP wire protocol, which means your existing StackExchange.Redis client connects to it without a single line of code changing. But compatibility is just the starting point. Garnet was built from the ground up on modern .NET, and it consistently outperforms Redis in throughput, latency, and multi-threaded scalability. It's not a toy research project either — it's backed by Microsoft Research, available as a NuGet package, and already powering workloads internally at Microsoft.
Why another cache store?
Redis is single-threaded by design. That was a deliberate architectural choice, and for years it was good enough — you scaled horizontally with clustering and accepted the per-node throughput ceiling. But modern server hardware has 32, 64, or 128 cores, and leaving all but one of them idle for cache operations is increasingly hard to justify.
Garnet takes a different approach. Its storage layer, called Tsavorite, is thread-scalable from the start. Under write-heavy workloads, benchmarks show Garnet achieving up to 108% higher throughput than Redis at 500 concurrent connections. At the tail end of the latency spectrum — the 99.9th percentile that actually matters in production — Garnet consistently delivers sub-300 microsecond latencies on commodity Azure VMs with accelerated networking.
The practical implication: you may be able to serve the same workload with fewer, smaller cache nodes.
Getting started
Garnet can be run in three ways: as a standalone server from source, as a .NET tool, or embedded directly in your application as a library.
Running as a .NET tool
The simplest way to get a Garnet instance running locally:
dotnet tool install --global garnet-server
garnet-server
By default, Garnet listens on TCP port 6379 — the same default as Redis. If you already have Redis running locally, specify a different port:
garnet-server --port 6380
Running from source
Clone the repository and run the server project:
git clone https://github.com/microsoft/garnet.git
cd garnet/main/GarnetServer
dotnet run -c Release -f net10.0
You can configure the initial hash index size, which affects memory usage and lookup performance:
dotnet run -c Release -f net10.0 -- -i 512m
Embedding in your application
For scenarios where you want the cache in-process — integration tests, edge deployments, or applications where a separate cache process adds unnecessary complexity — Garnet ships as a NuGet package:
using Garnet;
using var server = new GarnetServer(args);
server.Start();
// Server is now listening — your application continues running
await host.RunAsync();
The Microsoft.Garnet package gives you the full Garnet server as a library. The GarnetServer class accepts the same command-line arguments as the standalone tool, or you can pass a GarnetServerOptions object for programmatic configuration.
Connecting from your .NET application
Because Garnet speaks RESP, you use StackExchange.Redis exactly as you would with Redis. No new client library to learn, no new abstractions:
public class CacheService(IConnectionMultiplexer redis)
{
public async Task<string?> GetAsync(string key)
{
var db = redis.GetDatabase();
return await db.StringGetAsync(key);
}
public async Task SetAsync(string key, string value, TimeSpan? expiry = null)
{
var db = redis.GetDatabase();
await db.StringSetAsync(key, value, expiry);
}
}
Your dependency injection setup doesn't change either:
builder.Services.AddSingleton<IConnectionMultiplexer>(
ConnectionMultiplexer.Connect("localhost:6379"));
builder.Services.AddSingleton<CacheService>();
If you're currently using Redis, you can point your connection string at a Garnet instance and test without touching application code. That's the entire migration for basic usage.
Using Garnet with HybridCache
.NET's HybridCache — which went GA with .NET 9 — works with any IDistributedCache implementation. Since Garnet is RESP-compatible, you can use the standard Redis distributed cache provider and point it at Garnet:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "myapp:";
});
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
LocalCacheExpiration = TimeSpan.FromMinutes(5),
Expiration = TimeSpan.FromMinutes(30)
};
});
Your application gets an L1 in-memory cache with Garnet as the L2 distributed backing store, all through the standard Microsoft caching abstractions.
Configuration that matters
Garnet supports configuration through command-line arguments, a garnet.conf JSON file, or even a Redis-compatible redis.conf format. Here are the settings you'll care about most.
Memory and storage
The -m flag controls total log memory. The --index flag sets the initial hash table size. Both values are automatically rounded down to the nearest power of 2:
garnet-server -m 4g --index 1g
Persistence
Garnet supports append-only file (AOF) logging for durability, similar to Redis AOF:
garnet-server --aof --aof-commit-freq 1000
The --aof-commit-freq value is in milliseconds. For stronger durability guarantees, you can enable --aof-commit-wait to wait for the flush to complete before acknowledging client writes, at the cost of higher latency.
Checkpointing is also available for point-in-time snapshots:
garnet-server --checkpointdir /data/checkpoints --recover
The --recover flag tells Garnet to restore from the latest checkpoint on startup.
Storage tiering
One of Garnet's standout features is transparent storage tiering. When your dataset exceeds available memory, Garnet can spill to local SSD or Azure Storage without changing your client code:
garnet-server --storage-tier --logdir /data/garnet-tier
This is particularly useful for workloads with a hot working set that fits in memory and a long tail of less-frequently accessed keys that can tolerate SSD latency.
TLS
Production deployments should enable TLS:
garnet-server --tls --cert-file-name /certs/garnet.pfx --cert-password "${CERT_PASSWORD}"
Authentication
Garnet supports password authentication and ACL files for fine-grained access control:
garnet-server --auth Password --password "${GARNET_PASSWORD}"
For more complex setups, use an ACL file:
garnet-server --auth ACL --acl-file /etc/garnet/users.acl
Custom commands in C#
This is where Garnet diverges most from Redis and where .NET developers will find the most interesting capability. Instead of writing Lua scripts, you extend Garnet with native C# code.
Garnet supports five extension points: custom raw string commands, custom object commands, custom transactions, custom procedures, and modules. Custom raw string commands operate on the main key-value store. You implement them by inheriting from CustomRawStringFunctions:
public class SetIfGreater : CustomRawStringFunctions
{
public override bool Reader(
ReadOnlySpan<byte> key,
ReadOnlySpan<byte> input,
ReadOnlySpan<byte> value,
ref (IMemoryOwner<byte>, int) output,
ref ReadInfo readInfo)
{
// Return the current value
CopyTo(value, ref output);
return true;
}
public override bool NeedInitialUpdate(
ReadOnlySpan<byte> key,
ReadOnlySpan<byte> input,
ref (IMemoryOwner<byte>, int) output)
=> true;
public override int GetInitialLength(ReadOnlySpan<byte> input)
=> input.Length;
public override bool InitialUpdater(
ReadOnlySpan<byte> key,
ReadOnlySpan<byte> input,
Span<byte> value,
ref (IMemoryOwner<byte>, int) output,
ref RMWInfo rmwInfo)
{
input.CopyTo(value);
CopyTo(value, ref output);
return true;
}
public override bool InPlaceUpdater(
ReadOnlySpan<byte> key,
ReadOnlySpan<byte> input,
Span<byte> value,
ref int valueLength,
ref (IMemoryOwner<byte>, int) output,
ref RMWInfo rmwInfo)
{
// Only update if the new value is numerically greater
var currentVal = NumUtils.BytesToLong(value);
var newVal = NumUtils.BytesToLong(input);
if (newVal > currentVal)
{
input.CopyTo(value);
valueLength = input.Length;
}
CopyTo(value[..valueLength], ref output);
return true;
}
}
Register it on the server side:
server.Register.NewCommand("SETIFGT", 1, CommandType.ReadModifyWrite, new SetIfGreater());
Your clients can then call this command through StackExchange.Redis:
var db = redis.GetDatabase();
var result = await db.ExecuteAsync("SETIFGT", "my-counter", "42");
The advantage over Lua scripts is significant: you get full C# type safety, proper debugging, access to NuGet packages, and the performance of compiled .NET code rather than an interpreted scripting language.
Cluster mode
Garnet supports sharded clustering with the same semantics as Redis Cluster. Enable it with:
garnet-server --cluster --port 7000
Cluster management uses standard Redis cluster commands (CLUSTER MEET, CLUSTER ADDSLOTS, etc.), so existing cluster management tooling works. Garnet supports replication, failover, and live key migration between nodes.
// NOTE
Garnet's cluster implementation uses a passive design — it responds to control plane commands rather than making autonomous decisions. You'll need an external orchestrator or manual commands to manage slot assignments and failover, similar to how you'd manage a Redis Cluster without Sentinel.
When to consider Garnet
Garnet is a strong candidate if any of these apply to your situation:
- You're on .NET already. The operational story is simpler when your cache server runs on the same runtime as your application. Same build tooling, same deployment pipeline, same monitoring.
- You need better throughput per node. If you're scaling Redis horizontally primarily because individual nodes can't keep up, Garnet's thread-scalable architecture may let you consolidate.
- You want extensibility without Lua. Writing cache-side logic in C# with proper tooling support is a meaningfully better developer experience.
- Storage tiering matters. If your dataset is larger than available memory and you'd rather tier to SSD than evict, Garnet handles this natively.
- The Redis licence concerns you. Garnet is MIT-licensed. No dual-licensing, no source-available restrictions.
Common pitfalls
Assuming full Redis command parity. Garnet covers a large surface area of RESP commands, but not every Redis command is implemented. Check the supported commands documentation before migrating a workload that uses niche Redis features. Lua scripting is supported, but some advanced scripting patterns may behave differently.
Ignoring index sizing. Garnet's hash index size is set at startup and significantly affects both memory usage and lookup performance. Too small and you'll see hash collisions degrading throughput. Too large and you waste memory on a sparse table. Start with an index size roughly proportional to your expected key count and benchmark from there.
Skipping AOF in production. By default, Garnet runs with no persistence — a restart loses all data. If your workload requires durability, enable AOF and configure an appropriate commit frequency. The trade-off between --aof-commit-freq and --aof-commit-wait is the same as Redis: faster commits mean more durable data but higher write latency.
Not testing storage tiering latency. Storage tiering is powerful, but SSD-backed reads are orders of magnitude slower than memory reads. Profile your access patterns to ensure the hot set genuinely fits in memory. If your workload has a flat access distribution rather than a clear hot/cold split, tiering may hurt more than it helps.
Treating Garnet 2.0 beta as production-ready. The stable release is version 1.1.x. The 2.0 beta introduces breaking changes and is explicitly not recommended for production use. Stick to stable releases for anything that matters.
Summary
- Garnet is a RESP-compatible cache store from Microsoft Research, written in .NET, that outperforms Redis in throughput and tail latency benchmarks
- Your existing
StackExchange.Rediscode works with Garnet without modification — migration can be as simple as changing a connection string - It integrates with .NET's
HybridCacheandIDistributedCacheabstractions through the standard Redis cache provider - Custom server-side commands are written in C# instead of Lua, with full debugging and NuGet package support
- Storage tiering to SSD or Azure Storage lets datasets exceed available memory without eviction
- Cluster mode uses standard Redis cluster commands and is compatible with existing management tooling
- The project is MIT-licensed, actively maintained, and available both as a standalone server and as an embeddable NuGet package