New in v0.4.0

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

MethodWhat 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.