Dynamic Limits
Change rate limits at runtime. No restart, no redeploy. Updates propagate immediately on the local process and across the fleet when backed by a shared loader.
PolicyRegistry
In-process registry of named policies. update() takes effect on the next check no restart.
cachedPolicyResolver
Wrap a (possibly async) loader that reads Redis/DB/config limits propagate across instances within the TTL.
The resolver concept
Up to now, the limiter's policy was a static object. v0.4.0 makes it a resolver a function (sync or async) that returns the policy for the current request. Anything that produces a policy works: a registry lookup, a loader that hits Redis, your config service, a plan-aware mapping. Halt does the rest.
PolicyRegistry in-process, mutable
Create a registry, hand its resolver to the limiter, and call registry.update(name, …) whenever a limit needs to change. The next check sees the new value no restart, no reinitialisation.
TypeScript
import { RateLimiter, PolicyRegistry, presets } from 'halt-rate';
const registry = new PolicyRegistry([presets.PUBLIC_API]);
const limiter = new RateLimiter({
store,
policy: registry.resolver(() => 'public_api'),
});
// later flip the limit live
registry.update('public_api', { limit: 500 });Python
from halt import RateLimiter, PolicyRegistry, presets
registry = PolicyRegistry([presets.PUBLIC_API])
limiter = RateLimiter(
store=store,
policy=registry.resolver(lambda req: "public_api"),
)
# later flip the limit live
registry.update("public_api", limit=500)Registry API
| Method | What it does |
|---|---|
register(policy) | Add a new named policy. |
get(name) / has(name) | Look up an existing policy. |
update(name, …) | Patch a policy in place. Takes effect on the next check on every limiter using registry.resolver(). |
remove(name) | Drop a policy from the registry. |
list() | Iterate every registered policy. |
resolver(selector) | Returns a policy resolver. The selector picks a policy name per request. |
Burst recompute. If you raise limit past the old burst, update() recomputes a default burst automatically so you can't accidentally cap traffic below the new limit.
cachedPolicyResolver cross-fleet limits
For limits that need to change across instances (typical SaaS scenario: an admin flips a customer's plan and every server sees it within a few seconds), point a resolver at a shared source of truth and wrap it in cachedPolicyResolver.
TypeScript
import { RateLimiter, cachedPolicyResolver, presets } from 'halt-rate';
const policy = cachedPolicyResolver(
async (req) => loadPolicyFromRedis(planFor(req)), // your loader
{
ttlMs: 5_000, // refresh every 5s
key: planFor, // cache key derived from the request
},
);
const limiter = new RateLimiter({ store, policy });
const decision = await limiter.check(req);Python
from halt import RateLimiter, cached_policy_resolver
policy = cached_policy_resolver(
load_policy_from_redis, # your loader (sync or async)
ttl=5, # seconds
key=plan_for, # cache key derived from the request
)
limiter = RateLimiter(store=store, policy=policy)
# async loader => use acheck
decision = await limiter.acheck(request)The loader is your source of truth. Updating a row in Redis or your DB changes limits on every instance within the TTL no restart, no deploy.
Python async resolvers
The synchronous limiter.check() raises a clear error if handed an async resolver. Use await limiter.acheck(request) instead it awaits the resolver and works with both sync and async loaders. The FastAPI middleware already uses acheck internally, so async resolvers work end-to-end out of the box.
Picking the right tool
Use PolicyRegistry when…
- • You change limits from inside the same process (admin endpoint, hot-reload).
- • You have a small known set of named policies.
- • You don't need fleet-wide propagation.
Use cachedPolicyResolver when…
- • Limits live in Redis / a DB / a config service.
- • You need updates to propagate across many app instances.
- • Per-tenant / per-plan policies that an operator flips outside the app.
See also
- Plan-based limiting combine plan presets with a registry for per-tier limits with operator override.
- Observability every policy change shows up in telemetry tagged with the policy name and plan.