Two things ship from this repository:
fastcached— a fast in-memory cache daemon that speaks the memcached text, memcached binary, memcached meta, and Redis RESP2 protocols on a single port, auto-detecting which one each client is using from its first bytes. On the in-memory GET workload a compile cache cares about, it benchmarks faster than native redis and memcached (see Benchmarks).fastcache-cc— a drop-in compiler launcher in the style of ccache and sccache, backed byfastcached. Unlike those, its cache entries are portable across checkout paths, so CI runners and developer machines share one cache even when their source trees live at different depths.
Use them together for a shared compile cache, or run fastcached on its own as
a memcached/Redis-compatible cache — including as a plain
sccache backend.
It is not a general-purpose replacement for memcached or Redis: it implements only the slice of each protocol a cache backend needs. It is in production use as a shared compile cache for a large C++ codebase, backing both CI runners and developer machines.
Full documentation: https://lastrada-software.github.io/fastcached/
Released packages — .deb, .rpm, macOS .pkg/.dmg, Windows .msi —
install both executables and register the daemon to start automatically.
On macOS the installer offers the launchd job as a per-user agent (default)
or a system-wide daemon, and ships fastcached-uninstall to remove it.
Or build it yourself; a build installs both executables into bin/:
cmake --preset clang-release
cmake --build --preset clang-release
cmake --install out/build/clang-release --prefix /usr/localOr run the daemon in a container, built from the included Dockerfile:
docker build -t fastcached .
docker run --rm -p 11211:11211 fastcachedRequires CMake 3.28+, a C++23 compiler, and Ninja. Full details — presets, dependencies, and platform notes — in Install.
fastcached --port=11211Then talk to it with any memcached or Redis client. These transcripts are copied from the unit tests, so they reflect exactly what the server emits:
> set foo 0 0 5\r\nhello\r\n
< STORED\r\n
> get foo\r\n
< VALUE foo 0 5\r\nhello\r\nEND\r\n
> *3\r\n$3\r\nSET\r\n$1\r\nk\r\n$5\r\nhello\r\n
< +OK\r\n
> *2\r\n$3\r\nGET\r\n$1\r\nk\r\n
< $5\r\nhello\r\n
Protocol coverage is intentionally a subset — the commands a cache client actually uses. See the coverage matrix for the exact list.
fastcache-cc fronts each compile: on a hit it reproduces the object file and
replays the compiler's output; on a miss it runs the real compiler and stores
the result. If anything goes wrong — daemon down, network gone, cache
corrupt — it silently runs the real compiler. A broken cache can slow your
build down, never break it.
Raise the value cap: real object files routinely exceed the 16 MiB default (a large C++ codebase was measured at ~122 MB for its biggest object).
fastcached --port=11211 \
--storage=$HOME/.cache/fastcached/cache.cow \
--storage-max-value=256MPoint every machine that should share the cache at that one daemon.
Three variables must all be set, or every compile runs uncached:
export FASTCACHE_ADDR=127.0.0.1:11211 # the daemon
export FASTCACHE_SRCROOT=$PWD # your checkout root
export FASTCACHE_BUILDTREE=$PWD/build # your build directory
export FASTCACHE_COHORT=myproject-main # optional: prefetch groupingSRCROOT and BUILDTREE are what make entries portable: paths under them are
rewritten to tokens before hashing, so the same source at
/home/alice/proj and /ci/runner/w/1/s/proj produces the same cache key.
cmake -S . -B build -G Ninja \
-DCMAKE_C_COMPILER_LAUNCHER=fastcache-cc \
-DCMAKE_CXX_COMPILER_LAUNCHER=fastcache-cc
cmake --build build # first run: misses, populates the cache
rm -rf build/CMakeFiles/*.dir
cmake --build build # second run: hitsIt also works as a plain prefix for a single compile, which is the quickest way to check your setup:
FASTCACHE_VERBOSE=1 fastcache-cc g++ -c src/a.cpp -o build/a.o
# fastcache-cc: MISS key=05b1b5ef9ef5135e119560a2aa140aef
# fastcache-cc: STORED key=05b1b5ef9ef5135e119560a2aa140aef bytes=1427With PowerShell and MSVC:
$env:FASTCACHE_ADDR = '127.0.0.1:11211'
$env:FASTCACHE_SRCROOT = $PWD
$env:FASTCACHE_BUILDTREE = "$PWD\build"
cmake -S . -B build -G Ninja `
-DCMAKE_CXX_COMPILER_LAUNCHER=fastcache-cc.exefastcache-cc --show-stats # totals, hit rate, latency distributions
fastcache-cc --show-stats --cohort ci-main # one cohort only
fastcache-cc --zero-stats # discard the logcompiles : 4
hits : 2 (66.7% of 3 cacheable)
misses : 1
unavailable : 1 (25.0% of all compiles -- CACHE NOT REACHED)
fall-back reasons
1x connect failed
A non-zero unavailable count means the cache was never reached — check
FASTCACHE_ADDR and the daemon before concluding caching does not help.
Supported compilers: gcc/g++, clang/clang++ (including versioned names
like g++-14), and MSVC cl / clang-cl. The full reference — every
environment variable, exit codes, how the key is computed, and the known
limitations — is in the
fastcache-cc docs.
If you already use sccache, you can keep it and just point it at fastcached:
fastcached --port=11211 &
export SCCACHE_MEMCACHED=tcp://127.0.0.1:11211
sccache g++ -std=c++23 -c hello.cpp -o hello.o # miss
sccache g++ -std=c++23 -c hello.cpp -o hello.o # hit
sccache --show-statssccache up to 0.7.x talks to memcached over the text protocol; sccache ≥ 0.8
talks binary. Both work, because the listener detects the wire format from
the first bytes the client sends. SCCACHE_REDIS works the same way. CI
exercises all three protocols on every build.
Note that sccache keys on absolute paths, so its entries are not portable
between checkouts at different paths — that is the reason fastcache-cc exists.
Run it authenticated, encrypted, and monitored:
# Container: cache on 11211, Prometheus /metrics + /healthz on 9259.
docker run --rm -p 11211:11211 -p 9259:9259 fastcached \
--bind=0.0.0.0 --metrics --metrics-bind=0.0.0.0 --requirepass=secret
# With TLS (needs an OpenSSL build):
fastcached --requirepass=secret --tls --tls-cert=server.crt --tls-key=server.key \
--metrics
redis-cli --tls --insecure -a secret ping # -> PONG
curl http://127.0.0.1:9259/healthz # -> 200 OKBinding 0.0.0.0 without --requirepass exposes the cache to the network —
pair them. The image's HEALTHCHECK calls fastcached --healthcheck, a
self-contained probe needing no curl in the image. Full guide, including a
Kubernetes manifest with liveness/readiness probes, in the
deployment docs.
Run fastcached --help for the complete, always-current flag list. The ones
that matter most:
| Flag | Purpose |
|---|---|
--bind=<addr> / --port=<num> |
Where to listen (default 127.0.0.1:11211). |
--max-memory=<size> |
In-memory budget; k/m/g suffixes, or N% of host RAM (default 64 MiB). |
--storage=<path> |
Persist to a crash-consistent copy-on-write B+tree; without it the cache is memory-only. |
--storage-max-value=<size> |
Per-value cap, and the wire payload cap with it (default 16 MiB). Raise it for compile caches. |
--storage-durability=<mode> |
fsync / batched (default) / none. |
--threads=<N> |
Independent pinned reactors; the server's across-core parallelism. |
--requirepass=<secret> |
Require authentication (Redis AUTH, memcached SASL PLAIN). |
--metrics |
Serve Prometheus /metrics and /healthz. |
Every flag can also come from a YAML file via --config=<path>, with CLI flags
taking precedence; SIGHUP (POSIX) or the service manager's PARAMCHANGE
(Windows) re-reads it. On Windows, --install-service registers the daemon with
the Service Control Manager. See the
configuration docs.
With --storage, every commit is crash-consistent: the file always matches
either the previous or the new transaction, and a kill -9 at any instant
leaves no half-written state. Restarting with the same flags picks the cache
back up with no warm-up.
Storage is composed as an in-memory LRU (L1) over the on-disk B+tree (L2), and
sharded by key hash when --storage-shards>1 so writes to different shards
never block each other. --threads=N runs N single-threaded reactors, each
pinned to its own core, with every connection pinned to one reactor for its
lifetime — so the server scales across cores without cross-thread coroutine
migration. Details in the
architecture docs.
The repo ships a reproducible suite (bench/) that drives
fastcached and native redis-server / memcached through the same scenarios.
In-memory GET throughput on an AMD Ryzen 9 9950X3D (16C/32T, 96 GB), median of 3 reps, both competitors run as native binaries:
| Concurrency | fastcached | vs native redis | vs native memcached |
|---|---|---|---|
| 1 | ~120k ops/s | ~1.0× (tie) | ~1.0× (tie) |
| 16 | ~900k ops/s | 2.5× | ~1.0× (tie) |
| 64 | ~1.4M ops/s | 3.7× | 1.6× |
| 256 | ~1.2M ops/s | 4.7× | 1.5× |
Geomean across the small-value in-memory scenarios is ~2.7× redis and ~1.35× memcached, with 0 errors and 0 timeouts across the sweep. At a single connection there is no parallelism to exploit and all three tie; fastcached's per-core reactors pull ahead of single-threaded redis from 16 connections up and overtake native memcached at 64+. The persistent backend sustains ~11k durable SET ops/s at one connection and ~67k at 16, p99 under 0.5 ms.
These are honest but narrow numbers: one fast desktop CPU, single machine. The
redis baseline is the native single-threaded build, so a modern io-threads
redis would narrow the network gap — the multi-core architecture advantage is
what stands. Reproduce with python bench/fastcached_bench.py --vs redis,memcached.
See AGENT.md for the architecture, error taxonomy, coding
guidelines, and how to build and test the project.
Licensed under the Apache License, Version 2.0. See LICENSE.
Memcached and Redis are registered trademarks of their respective owners.
