Cache & Rate Limiting
Two of the most common things you build on a key-value store are a cache and a rate limiter, so Fast ships both as thin, friendly wrappers over the KV store. Because they ride on KV, they need use_kv() enabled first.
The cache
cache() is a small veneer over the store. Its entries live under a cache: prefix, so they never collide with keys you set directly through kv(). The star of the show is remember(), which returns a cached value or computes and stores it on a miss:
$report = cache()->remember('report', ttl: 30, callback: fn () => build_report());
The first call runs build_report() and caches the result for 30 seconds; subsequent calls within that window return the cached value without recomputing. The rest of the API is what you would expect:
cache()->set('k', $value, ttl: 60);
cache()->get('k', $default);
cache()->has('k');
cache()->forget('k');
cache()->increment('views');
A couple of sharp edges worth knowing:
A literal
nullis not cacheable throughremember(); it reads as a miss and your callback runs again. And there is deliberately nocache()->flush(): since the store is shared, a global flush would wipe unrelated keys. Clear entries individually withforget().
Rate limiting
The rate limiter uses fixed-window counters over KV. The window and its expiry are created together atomically, so a counter is never left stranded without a TTL. You can apply it two ways.
As global middleware, throttle() returns a middleware that enforces a limit and sets the right headers (a 429 with Retry-After and X-RateLimit-*) when exceeded:
use_middleware(throttle(max: 60, decay: 60)); // 60 requests per 60s, keyed by client IP
By default it keys on the client's IP. To limit by something else, pass a by resolver that returns the key for a request:
use_middleware(throttle(10, 60, by: fn ($r) => 'path:' . $r->path));
Inline limiting
Sometimes you want to rate-limit one specific action rather than a whole route, like login attempts. Use limiter() directly inside a handler:
if (!limiter()->attempt("login:$ip", max: 5, decay: 60)) {
throw new Fast\RateLimit\TooManyRequestsException(
limiter()->retryAfter("login:$ip")
);
}
attempt() returns false once the limit is hit, and retryAfter() tells you how many seconds until the window resets, which the exception turns into a proper Retry-After header.
A note on proxies
The default rate-limit key is the direct-peer client IP, with the port stripped. If your app sits behind a proxy or load balancer, that direct peer is the proxy, not the real client, so every request would share one bucket.
Behind a proxy, key the limiter on a trusted forwarded header instead of the direct peer, using the
byresolver. Only trust a forwarded header when you control the proxy setting it, or a client could spoof it to dodge limits.
Because Fast's router has no per-route middleware, the by resolver is also how you scope a throttle to specific paths or user identities.
The last pure utility is input validation, up next in Validation.