Key-Value Store
Fast ships a small, opt-in, in-memory key-value store: think of it as a tiny Redis that lives inside your app with no external server to run. It gives you atomic counters, a cache, locks, hashes, and lists, all shared across every worker.
How it works
When you enable it, the master process forks a set of shard processes, and every worker talks to them over sockets. Because each shard is a single process applying one operation at a time, every operation is atomic with no locking on your part. Keys are spread across shards by a hash, so load spreads out across cores.
Enabling it
Turn it on with use_kv() before serve(). Like every Fast subsystem, it is zero-tax when a request does not touch it:
use Fast\Kv\KvConfig;
use_kv(new KvConfig(shardCount: 2));
Then reach for it with the kv() helper:
post('/hit', fn () => ['n' => kv()->incr('hits')]); // atomic across all workers
get('/greet', function () {
if (($cached = kv()->get('greeting')) !== null) {
return $cached;
}
kv()->set('greeting', $greeting = build_greeting(), ttl: 60);
return $greeting;
});
That counter is genuinely atomic even under heavy concurrency across many workers, because the increment happens inside a single shard process.
Values, counters, and TTLs
The core value operations look like a familiar cache:
kv()->set('key', 'value', ttl: 60); // optional TTL in seconds
kv()->get('key');
kv()->getex('key', ttl: 60); // get and refresh the TTL
kv()->setnx('key', 'value'); // set only if absent
kv()->getset('key', 'new'); // set and return the old value
kv()->has('key');
kv()->delete('a', 'b', 'c');
kv()->mget('a', 'b');
kv()->mset(['a' => 1, 'b' => 2], ttl: 30);
kv()->incr('counter'); kv()->decr('counter');
kv()->expire('key', 60); kv()->ttl('key'); kv()->persist('key');
Locks
The store gives you an advisory distributed lock. lock() returns an owner token, and unlock() only releases the lock if you present that same token, so you can never accidentally drop someone else's lock:
$token = kv()->lock('job:nightly', ttl: 30);
if ($token !== null) {
try {
run_nightly_job();
} finally {
kv()->unlock('job:nightly', $token);
}
}
Locks are advisory and non-durable: a shard restart drops any held locks.
Hashes and lists
Beyond plain values, you get two structured types. Hashes are field-value maps:
kv()->hset('user:1', 'name', 'Ada');
kv()->hget('user:1', 'name');
kv()->hgetall('user:1');
kv()->hincr('user:1', 'logins');
kv()->hkeys('user:1'); kv()->hlen('user:1'); kv()->hdel('user:1', 'name');
Lists behave like Redis lists, including negative indices in lrange:
kv()->rpush('queue', 'job-1');
kv()->lpush('queue', 'urgent');
kv()->lpop('queue'); kv()->rpop('queue');
kv()->lrange('queue', 0, -1); // the whole list
kv()->llen('queue');
Things to keep in mind
The store is fast and convenient, but it has clear boundaries you should design around:
It is in-memory and non-durable: a shard restart drops that shard's data, so never treat it as a database. Values are scalars and arrays only (objects do not cross the process boundary, which is safe by default; reduce them to arrays or JSON yourself). Single-key operations are atomic, but multi-key ones like
delete,mget,mset, andflushfan out across shards and are not atomic across shard boundaries. Memory is bounded bymaxEntriesand an eviction policy, so one runaway writer cannot OOM a shard.
Two of the most useful things built on top of the store are the cache and the rate limiter, covered next in Cache & Rate Limiting.