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:

terminal
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:

terminal
garnet-server --port 6380

Running from source

Clone the repository and run the server project:

terminal
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:

terminal
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:

Program.cs
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:

Services/CacheService.cs
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:

Program.cs
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:

Program.cs
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:

terminal
garnet-server -m 4g --index 1g

Persistence

Garnet supports append-only file (AOF) logging for durability, similar to Redis AOF:

terminal
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:

terminal
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:

terminal
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:

terminal
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:

terminal
garnet-server --auth Password --password "${GARNET_PASSWORD}"

For more complex setups, use an ACL file:

terminal
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:

Extensions/SetIfGreater.cs
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:

Example.cs
server.Register.NewCommand("SETIFGT", 1, CommandType.ReadModifyWrite, new SetIfGreater());

Your clients can then call this command through StackExchange.Redis:

Example.cs
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:

terminal
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:

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