Open-source project
valkey-io/valkey-go avatar
valkey-io/valkey-go

valkey-go: a Go client that batches your commands and caches on the client

A fast Golang Valkey client that supports Client Side Caching, Auto Pipelining, and RMDA.

694 stars95 forksGoApache-2.0

At a glance

What is it?
valkey-go is the Go client for the Valkey key-value store, with automatic pipelining of concurrent commands, server-assisted client-side caching, distributed locks, rate limiters and an OpenTelemetry integration. It suits a Go service that talks to Valkey under concurrency, and its auto pipelining changes how you should think about per-command latency.
Who is it for?
valkey-go is worth adopting for a Go service that issues many concurrent commands against Valkey and is currently paying one round trip per command, because the auto pipelining path batches those commands without the caller changing anything, and because the client-side caching mode, the lock helper, the rate limiter and the OpenTelemetry integration are in the same client rather than in separate libraries.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The client batches commands you never asked it to batch

valkey-go is described as a fast Golang Valkey client that supports Client Side Caching, Auto Pipelining and RDMA, and the second of those is the one that changes how you write the code.

The feature list puts it first: auto pipelining for non-blocking Valkey commands. The mechanism is that all concurrent non-blocking commands, such as GET and SET, are automatically pipelined by default, which reduces the overall round trips and system calls and gets higher throughput. You get the benefit of the pipelining technique by calling client.Do() from multiple goroutines concurrently, and the documentation's benchmark shows exactly that shape, with b.RunParallel issuing the same GET from many goroutines.

So the call site does not change. You keep calling Do() the way you always did, and if your program happens to be doing things concurrently, the client coalesces the traffic into fewer round trips. The cost is that the batching happens at a layer you do not control, which is the trade the next section is about.

The feature list also names a Go-redis like API adapter, contributed by @418Coffee, and a Go-redis redis_rate parity adapter, plus Pub/Sub, Sharded Pub/Sub, Streams, Valkey Cluster, Sentinel, RedisJSON, RedisBloom, RediSearch and RedisTimeseries support.

The repository layout is a flat list of Go files at the root, one per concern: client.go, cluster.go, standalone.go, sentinel.go, pool.go, pipe.go, cache.go, lru.go, retry.go, pubsub.go, message.go, resp.go, cmds.go and helper.go, with the subpackages om/, mock/, valkeyaside/, valkeylock/, valkeylimiter/, valkeyotel/, valkeyhook/, valkeycompat/, valkeycompatrate/ and valkeyprob/ for the higher level pieces.

Auto pipelining trades latency for throughput, and can be turned off

The documentation is honest about the downside of auto pipelining in a way that is worth reading before you adopt it.

While auto pipelining maximizes throughput, it relies on additional goroutines to process requests and responses and may add some latencies due to goroutine scheduling and head of line blocking. That is a real characteristic of the design and not a footnote. A service that cares more about the latency of a single read than about aggregate throughput is making a bad trade by leaving this on.

The escape hatch is DisableAutoPipelining, set to true, which switches to a connection pooling approach and serves each request with a dedicated connection on the same goroutine. And it is not all or nothing, because when DisableAutoPipelining is set you can still send specific commands for auto pipelining with ToPipe():

golang
cmd := client.B().Get().Key("key").Build().ToPipe()
client.Do(ctx, cmd)

So a project can use connection pooling by default and opt into auto pipelining for a subset of requests. That is a more useful shape than a global switch, and it is the shape the documentation presents.

Manual pipelining is the third option, through DoMulti(), which takes a slice of commands and returns their responses:

golang
cmds := make(valkey.Commands, 0, 10)
for i := 0; i < 10; i++ {
    cmds = append(cmds, client.B().Set().Key("key").Value("value").Build())
}
for _, resp := range client.DoMulti(ctx, cmds...) {
    if err := resp.Error(); err != nil {
        panic(err)
    }
}

The cost of the command builder is in the same section, and it is the first thing that will bite you. When using DoMulti() to send multiple commands, the original commands are recycled after execution by default, and the documentation warns in bold that you should not reuse a command in another client.Do() or client.DoMulti() call because it has been recycled to the underlying sync.Pool. If you need to reference a command afterwards, such as to retrieve the key, use the Pin() method, which prevents the recycling. The pinned version of the same loop uses .Build().Pin() and then indexes back into cmds[i].Commands()[1] to recover the key.

Client-side caching is where the higher level helpers live

The opt-in mode of server-assisted client-side caching is enabled by default and is used by calling DoCach..., and this is the mechanism the rest of the crate's helpers are built on.

The subpackages in the tree tell you the scope. om/ is generic object mapping with client-side caching, valkeyaside/ is the cache-aside pattern with client-side caching, valkeylock/ is distributed locks with client-side caching, and valkeylimiter/ is distributed rate limiting with GCRA and Fixed Window algorithms. There is also mock/ for helpers for writing tests with a Valkey mock, valkeyotel/ for the OpenTelemetry integration, valkeyhook/ for hooks and other integrations, and valkeyprob/ for probabilistic data structures without Redis Stack.

The pattern is worth understanding because it is the reason the crate is more than a protocol wrapper. A distributed lock, a rate limiter and a cache-aside helper all need the same thing, which is a guarantee about when a cached value might be stale, and that guarantee comes from the server telling the client when to invalidate. A client-side cache without server-side invalidation is a correctness problem, so the tracking mechanism the server provides is not an optimisation here, it is what makes the helpers safe to expose.

The GCRA and Fixed Window rate limiting algorithms in valkeylimiter/ are the two usual choices, and the fixed window one is the simpler but cruder of the pair, since a fixed window allows a burst across the window boundary. The crate documents both, which is the right level of detail for a library that offers them as a choice.

There is also a color palette, an LRU implementation, a singleflight helper, a retry helper, a flow buffer, a multiplexer and a binary codec at the top level, so the crate carries a fair amount beyond the client itself.

The builder is the API, and Pin() is the rule you must not break

client.B() is the builder entry point to construct a Valkey command, and once a command is built you use either client.Do() or client.DoMulti() to send it.

The getting started example is the whole shape of the library:

golang
package main

import (
  "context"
  "github.com/valkey-io/valkey-go"
)

func main() {
  client, err := valkey.NewClient(valkey.ClientOption{InitAddress: []string{"127.0.0.1:6379"}})
  if err != nil {
    panic(err)
  }
  defer client.Close()

  ctx := context.Background()
  // SET key val NX
  err = client.Do(ctx, client.B().Set().Key("key").Value("val").Nx().Build()).Error()
  // HGETALL hm
  hm, err := client.Do(ctx, client.B().Hgetall().Key("hm").Build()).AsStrMap()
}

The builder is generated, so the method names are the command names in lower case, and the result types are typed: AsStrMap() for a map, ToString() for a string, Error() to check the error. Each command carries its own options, so Set() takes .Key(), .Value() and .Nx().

The recycling rule is the sharp edge, and it is stated in the documentation in the strongest terms: you should not reuse the command to another client.Do() or client.DoMulti() call because it has been recycled to the underlying sync.Pool by default. The escape is Pin() after Build(), which prevents the command from being recycled.

The reason is performance and it is worth understanding rather than memorising. A sync.Pool avoids an allocation per command by handing the object back for reuse, which is exactly what you want in a hot path, but it means a command you hold a reference to can be mutated out from under you by the next caller. Pin() takes the object out of that pool.

For the common case there are also the MGet and MGetCache helper functions to easily map keys to their corresponding responses, so a batch of reads does not need the manual index-and-recover pattern.

The test infrastructure runs against a zoo of servers

The docker-compose.yml in the repository is the most revealing file in it, because it describes what the client is actually expected to talk to.

It runs Valkey 8.0 Alpine, and it runs a large set of other servers alongside it: redis 5 Alpine, KeyDB 6.3.1, DragonflyDB 1.20.1, Apache Kvrocks 2.8.0, RediSearch 2.8.4, and the Redis Stack image at 7.4.0-v0. There is a compat service using the Redis Stack image and a compat5 service on redis 5, and a compat860 service on redis 8.6.0 Alpine, plus a compat-redisearch service on the Redis Stack image.

The point of the compat services is protocol compatibility. A client library for a fork of Redis that only worked against the fork would be a liability, and running the same test suite against Redis 5, Redis 8.6, KeyDB, Dragonfly, Kvrocks and RediSearch is how you find out where the protocol diverges rather than in production. Each service is mapped to a different host port so the test suite can address them individually.

Sentinel is covered too, with two setups: a Valkey sentinel monitoring a Valkey server on port 6380, and a Redis 5 sentinel, with the server started with --save "" --appendonly no so the sentinel configuration is not fighting with persistence, and a sentinel.conf written with the monitor line.

Cluster mode is present as well, with a Valkey server started with --cluster-enabled yes and a cluster config file.

The go.mod is minimal for a client library of this size: the module is github.com/valkey-io/valkey-go, it requires Go 1.25.0, and the only direct dependencies are gomega for assertions and golang.org/x/sys. The go-cmp and yaml packages are indirect. The circleci directory and the codecov badge show the test and coverage setup, and the root files binary_test.go, client_test.go, cluster_test.go and the rest of the paired test files show that the surface is tested per file.

Where valkey-go is the wrong tool

The first limit is that the auto pipelining default is wrong for some workloads. The documentation itself says it may add latency due to goroutine scheduling and head of line blocking, and if your service is dominated by a single sequential read whose latency the user feels, the default is costing you responsiveness. The escape exists, DisableAutoPipelining, plus ToPipe() for a middle ground, but the point is that you are choosing a default and you should be choosing it rather than inheriting it.

The second is the command recycling rule, and it is the failure mode most likely to appear in production rather than in a test. A built command that you keep a reference to and then pass to Do() again is being mutated underneath you, and the symptom is a command that sends the wrong key or the wrong value, intermittently, under load. The rule is simple, do not reuse a built command, or Pin() it, but it is a rule you have to know, and a team that writes a helper function that caches a built command will hit it.

The third is the server-side dependency of the caching helpers. The opt-in server-assisted client-side caching mode is enabled by default, and the lock, rate limit, cache-aside and object mapping helpers are built on it. That means the semantics of those helpers depend on the server tracking and invalidating correctly, and a deployment against a server or a proxy in front of it that does not handle the tracking protocol will get stale reads rather than an error. This is the cost of the design, and it is a good one for a controlled deployment and a bad one for a mixed fleet.

The fourth is the surface area. The crate carries cluster, sentinel, pubsub, streams, sharded pubsub, mock helpers, OpenTelemetry, hooks, rate limiting, locks, an LRU, a singleflight, a flow buffer, a binary codec and a multiplexer, plus a Go-redis compatibility adapter. A team that only needs a GET and a SET is taking on all of it, and the compatibility adapter is a translation layer with its own fidelity questions rather than the real thing.

The fifth is the benchmark claim's shape. The documentation states that valkey-go has higher throughput across 1, 8 and 64 parallelism settings than go-redis, and that it achieves around 14x throughput over go-redis in a local benchmark on a MacBook Pro 16 inch M1 Pro 2021 in the parallelism 64, key 16, value 64 case. That is a specific configuration on specific hardware with a specific dataset, and the benchmark source is a third party repository, so the number is a data point rather than a general result.

Editorial conclusion

valkey-go is worth adopting for a Go service that issues many concurrent commands against Valkey and is currently paying one round trip per command, because the auto pipelining path batches those commands without the caller changing anything, and because the client-side caching mode, the lock helper, the rate limiter and the OpenTelemetry integration are in the same client rather than in separate libraries. It is the wrong choice if your workload is a handful of sequential commands, and the wrong choice if you cannot accept a client that recycles its command objects. Before you adopt it, check the three things the documentation makes explicit: that a built command must not be reused across Do() calls unless you call Pin() on it, because the underlying sync.Pool recycles it; that auto pipelining relies on extra goroutines and can add latency through scheduling and head of line blocking, with DisableAutoPipelining and ToPipe() as the opt-outs; and that the pinned server-assisted client-side caching mode is on by default and is what the lock, rate limit and cache-aside helpers are built on, so a deployment against a server without tracking enabled needs that checked.

Frequently asked questions

What does auto pipelining in valkey-go do?

All concurrent non-blocking Valkey commands, such as GET and SET, are automatically pipelined by default, which reduces overall round trips and system calls and raises throughput. You get it by calling client.Do() from multiple goroutines concurrently, without changing the call site. The cost is that it relies on extra goroutines and may add latency from goroutine scheduling and head of line blocking, and it can be turned off with DisableAutoPipelining.

Why can I not reuse a command built with client.B()?

A built command is recycled to the underlying sync.Pool by default, so reusing it in another client.Do() or client.DoMulti() call means the command can be mutated underneath you. Call Pin() after Build() to prevent recycling, which matters when you need to reference a command afterwards, for example to retrieve the key from a batch sent with DoMulti().

What does valkey-go provide besides the core client?

Generic object mapping with client-side caching under om/, a cache-aside helper under valkeyaside/, distributed locks under valkeylock/, distributed rate limiting with GCRA and Fixed Window under valkeylimiter/, test helpers with a Valkey mock under mock/, an OpenTelemetry integration under valkeyotel/, hooks and other integrations under valkeyhook/, probabilistic data structures without Redis Stack under valkeyprob/, and Go-redis compatible adapters under valkeycompat/ and valkeycompatrate/.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. valkey-io/valkey-go on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/valkey-io-valkey-go.svg)](https://hysenlabs.com/projects/valkey-io-valkey-go)

Was this useful?