API · Rate limiting
Per-endpoint / per-subject / per-tenant rpc rate limiting with @voltro/plugin-ratelimit — a default fallback + rules (exact tag or regex), sliding-window / token-bucket algorithms, composite keying, and a typed RateLimited error auto-merged into the wire error union. Memory store, zero infra.
Throttle rpc calls without touching a handler. @voltro/plugin-ratelimit runs on the rpc interceptors — the one surface that sees every call AND the resolved subject. A default fallback applies everywhere; rules override per endpoint with a choice of algorithm and bucket key. Over-limit calls fail a typed RateLimited error the plugin merges into every procedure's wire error union. Memory store is single-node; postgres/redis for a cluster. Template id: api-ratelimit.
Scaffold
voltro create-project acme --api=api-ratelimitLimits as config
// app.config.ts
import { rateLimitPlugin } from '@voltro/plugin-ratelimit'
rateLimitPlugin({
default: { limit: 100, window: '1m' }, // global fallback, per subject
rules: [
// 3/min per tenant, token-bucket (starts full → burst of 3, then refills)
{ match: 'notes.create', limit: 3, window: '1m', algorithm: 'token-bucket', burst: 3, by: 'tenant' },
],
store: 'memory',
})match— an exact tag or a/regex/;kindnarrows to mutation/query/action.algorithm—sliding-window(default),fixed-window, ortoken-bucket(withburst).by— the bucket key:subject(default),tenant,apiKey,global, or a composite array.
Typed RateLimited
There's no rate-limit code in the handler — the interceptor enforces it. An over-limit call fails RateLimited ({ tag, limit, retryAfterMs, resetAtMs }), auto-merged into the procedure's wire error union, so the client decodes it typed and can show a "try again in N seconds".
Try it
# Fire notes.create four times for tenant 'acme' — the 4th trips the limit:
for i in 1 2 3 4; do
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d "{\"tag\":\"notes.create\",\"input\":{\"tenantId\":\"acme\",\"title\":\"n$i\",\"body\":\"x\"}}"
done
# → calls 1-3: { ok:true } ; call 4: { ok:false, error:{ _tag:"RateLimited", … } }Multi-node
store: 'memory' is single-process. Switch to 'postgres' (state under a row lock, reuses DB_*/PG_*) or 'redis' (fastest — one atomic Lua script; reads CACHE_REDIS_URL / REDIS_URL). All shared stores fail open on a backend error — a limiter outage degrades to "no limit", never to "deny everything".
Anti-patterns
- Rate-limiting reads you didn't mean to. The
defaultapplies to queries too. Usekind: 'mutation'on a rule (or a tighter default) if you only want to throttle writes. - Reusing the cache store as the limiter store. A cache is get/set; a limiter needs an ATOMIC increment. They're separate backends —
storehere is independent of@voltro/cache.