# sturdyc: a Go cache built to survive stampedes

> sturdyc is a caching library for Go aimed at a specific failure mode: a key expires, thousands of requests miss at once, and the backing store gets hammered. Its API is small and its configuration is large, because the options decide how stale data may be and when a refresh happens.

**viccon/sturdyc** — A caching library with advanced concurrency features designed to make I/O heavy applications robust and highly performant

- Repository: https://github.com/viccon/sturdyc
- Stars: 1,282 · Forks: 35
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/viccon-sturdyc

## The problem is a stampede, not a cache

The project describes itself as a sturdy gopher shielding data sources from rapidly incoming requests, and its first technical claim is that it eliminates cache stampedes and can minimize data source load in high throughput systems through features such as request coalescing and asynchronous refreshes.

A stampede is the specific failure this addresses. A key expires, many concurrent requests all miss, and all of them reach the database at once, which defeats the purpose of the cache. The usual answers are a lock, a single flight, or a short negative cache, and sturdyc offers the full set: inflight tracking, asynchronous refreshes and refresh coalescing.

The stated design is the speed of in-memory caching with granular control over data freshness. At its core the library provides non-blocking reads and sharded writes for minimal lock contention, and the xxhash algorithm is used for efficient key distribution, which is what makes the sharding work without scanning the whole key space.

The author says the package has been used in production for services that retrieve data from distributed caches, databases and external APIs, and that while the API surface is tiny, the configuration options are extensive. The documentation is organised as a table of contents and asks first time readers to work through them in order, because most of them build on each other and share configurations.

## Installing and creating a client with four numbers

The library is a normal Go module, so the install step is a single module fetch.

```sh
go get github.com/viccon/sturdyc
```

The next step is creating a cache client to hold your configuration. The constructor is variadic, and the first four arguments are the ones you will actually use.

```go
capacity := 10000
numShards := 10
ttl := 2 * time.Hour
evictionPercentage := 10

cacheClient := sturdyc.New[int](capacity, numShards, ttl, evictionPercentage)
```

The type parameter is the value type, and the four numbers are capacity, shard count, time to live and eviction percentage. The shard count is the concurrency knob, since more shards means fewer write lock collisions. The eviction percentage is a safety valve, because setting it to zero makes writes a no-op until an item has expired or been deleted. The time to live is the freshness contract, and everything else in the library is about adjusting when that contract is broken and what happens while it is.

## Two eviction strategies, and quickselect instead of an LRU list

The eviction story shows what the library values, and the algorithm is named explicitly.

One strategy is a background job that continuously evicts expired records from each shard, and its interval can be tuned with an option.

```go
cacheClient := sturdyc.New[int](capacity, numShards, ttl, evictionPercentage,
    sturdyc.WithEvictionInterval(time.Second),
)
```

The job can also be disabled entirely, which is said to give a slight performance boost in situations where you are unlikely to ever exceed the capacity you have assigned to your cache.

```go
cacheClient := sturdyc.New[int](capacity, numShards, ttl, evictionPercentage,
    sturdyc.WithNoContinuousEvictions()
)
```

The second strategy triggers when the capacity is reached. It performs evictions on a per-shard basis, selecting records for removal based on recency, and the algorithm used is quickselect, which has an O(N) time complexity without the overhead of requiring write locks on reads to update a recency list, as many LRU caches do.

That is a real trade. An LRU cache that updates a recency list on every read has to take a write lock on the read path, which would be self defeating in a library whose selling point is non-blocking reads. Quickselect gives approximate recency ordering without touching shared state on the read path, at the cost of doing work when a shard is already full.

## The feature list is really a configuration surface

The table of contents is a list of cache failure modes, and each entry corresponds to a set of options.

There are the basics, installing and creating a cache client, then evictions, get or fetch, stampede protection, early refreshes and deletions.

Then come the ones that make it more than a map with a time to live. Caching non-existent records, which is the negative cache that stops a missing key from being fetched on every request. Caching batch endpoints per record, so a list endpoint is cached per item rather than as one blob. Cache key permutations, for the case where a list of parameters maps to a key. Refresh coalescing, the mechanism behind the load reduction. Request passthrough, for values that should never be cached. Distributed storage, for when the cached data lives elsewhere. Custom metrics, because the library ships with instrumentation hooks. And generics, for the typed variants of the client.

The design goal behind all of it is that the package integrates with any data source, and the get-or-fetch section states that the API was designed to make that integration as straightforward as possible. The interface you implement is a fetch function, and the coalescing, the inflight tracking and the early refresh are handled by the library rather than by the caller. That is why the API surface stays small while the configuration stays large: the call site is simple and the behaviour of the cache is decided up front.

## The load reduction claim and what it rests on

Two quantitative claims and one visual result are offered, and it is worth being precise about what kind of evidence they are.

A screenshot shows the P95 latency improvements the author observed after adding the package in front of a distributed key-value store. That is a latency comparison on one workload, presented as an image rather than as a reproducible benchmark.

The load claim is the one that matters for the design. Through a combination of inflight tracking, asynchronous refreshes and refresh coalescing, the author says they reduced load on underlying data sources by more than 90 percent, and that this reduction in outgoing requests has enabled them to operate with fewer containers and significantly cheaper database clusters.

That last sentence is the real argument for the library. The saving is not latency, it is infrastructure, because a backend that receives far fewer requests can run on fewer instances, on smaller ones, or stop being the reason you need another cache tier in front of it.

The same framing runs through the whole library, which is aimed at making I/O heavy applications hold up under load, and the set of features in the table of contents is essentially the list of ways a cache can fail under traffic and the option that addresses each one.

## One file per concern, and a test suite built around the race detector

The source is organised one file per concern, which makes the feature list visible in the tree. There is cache.go, fetch.go for the data source side, inflight.go for the in-flight tracking that stops stampedes, refresh.go for refresh behaviour, shard.go for the sharding, keys.go for key derivation and permutations, buffer.go for buffering, distribution.go, passthrough.go, metrics.go, options.go, log.go, clock.go for time handling, errors.go, safe.go and quickselect.go for the eviction algorithm.

Most of those have a matching test file, and the examples directory holds nine example programs: basic, batch, buffering, distributed early refreshes, distribution, generics, missing, permutations and refreshes.

The Makefile shows the quality bar. The test target removes the test cache first and then runs go test with the race detector, vet disabled and a 15 second timeout, across all packages. The bench target runs the benchmarks, and the audit target tidies and verifies the module, formats the code, runs golangci-lint and then runs the race-enabled tests again.

The go.mod is short, with Go 1.22 and two dependencies, xxhash for key distribution and go-cmp as a test helper. For a library whose selling point is concurrency behaviour, running the suite under the race detector is the part of the build that matters most, and it is the first thing the Makefile does.

## Conclusion

sturdyc fits a Go service that reads from a slow or expensive backing store, such as a database, a distributed key-value store or a third party API, and whose traffic is bursty enough that a naive cache turns into a stampede. It does not fit a service with a handful of keys and no read amplification. Before adopting it, check three things: which eviction behaviour you want, since the background sweeper and the capacity-triggered eviction are separate mechanisms; how stale you are willing to serve, since the early refresh and refresh coalescing options trade freshness for load; and that the single dependency set is enough, since go.mod requires only xxhash and the test helper go-cmp.

## FAQ

### What problem does sturdyc solve in Go?

It eliminates cache stampedes and minimises data source load in high throughput systems, using request coalescing and asynchronous refreshes. At its core it provides non-blocking reads and sharded writes for minimal lock contention, with xxhash used for efficient key distribution.

### How do I create a sturdyc cache client?

With the variadic New function, passing the value type, the capacity, the number of shards, the time to live and the eviction percentage, for example sturdyc.New[int](capacity, numShards, ttl, evictionPercentage). Configuration options are passed after those arguments, and the shard count is the concurrency knob, since more shards means fewer write lock collisions.

### How does sturdyc evict entries?

Two strategies. A background job continuously evicts expired records from each shard, and its interval can be tuned with WithEvictionInterval, or the job can be disabled with WithNoContinuousEvictions. When the capacity is reached, evictions happen per shard based on recency, using quickselect, which has an O(N) time complexity without requiring write locks on reads to update a recency list.

### What advanced features does sturdyc offer?

The documented sections cover stampede protection, early refreshes, deletions, caching non-existent records, caching batch endpoints per record, cache key permutations, refresh coalescing, request passthrough, distributed storage, custom metrics and generics, with runnable examples in the repository's examples/ directory.

### How do I install sturdyc and what does it depend on?

With go get github.com/viccon/sturdyc. The module targets Go 1.22 and requires only two things, the xxhash library for key distribution and the go-cmp helper for tests, which is a small dependency surface for a library with this much configuration.

## Sources

- [Issues](https://github.com/viccon/sturdyc/issues)
- [License: MIT](https://github.com/viccon/sturdyc/blob/main/LICENSE)
- [README](https://github.com/viccon/sturdyc/blob/main/README.md)
- [Releases](https://github.com/viccon/sturdyc/releases)
- [viccon/sturdyc on GitHub](https://github.com/viccon/sturdyc)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/viccon-sturdyc
