TokenPolice
Docs
Troubleshooting

Unexpected blocks

A call was blocked you didn't expect — trace it back to the rule and the two-dial gate.

A call raised TokenPoliceBlockedError and you didn't expect it. Good news: a block is never silent or accidental — it takes two dials both set to enforce plus a rule that matched. Any one of those being off means nothing blocks. Here's how to trace it back.

The block carries its own explanation

The error names the rule that stopped the call. Catch it and read the fields:

from token_police import TokenPoliceBlockedError

try:
    answer = client.chat.completions.create(...)
except TokenPoliceBlockedError as e:
    print(e.reason)     # human-readable "why"
    print(e.rule_id)    # the exact rule that fired — look it up in the dashboard
    print(e.kind)       # "budget", or the loop detector that fired
import { TokenPoliceBlockedError } from "token-police";

try {
  const answer = await client.chat.completions.create({ ... });
} catch (e) {
  if (e instanceof TokenPoliceBlockedError) {
    console.log(e.reason, e.ruleId, e.kind);
  }
}

Take rule_id to the rules table in your dashboard to see exactly what that rule matches and what it does. Full field reference: Errors.

Why it fired when you didn't expect it

A rule is scoped to a plan or user you're testing as. A rule like "IF paid_plan is free" only blocks free-plan traffic. Check the user_id / paid_plan your code is attaching — if a test account maps to a capped plan, the block is correct.

The plan string on the call isn't the one you think it is. Matching is exact: case- and whitespace-sensitive, with no null tier.

What your app sends as paid_planWhat a rule "IF paid_plan is free" does
nothing at allcounts as "free" — the rule fires, including for paying users, and their spend eats the free-tier allowance
"Free", "FREE", " free "never matches — that spend is invisible to this rule, and a rule "IF paid_plan is not free" fires for them instead

Attach paid_plan on every call and normalize it once at the call site (str(user.plan or "free").strip().lower()), then use that same spelling in every rule. More in Identity.

The rule has no limit. A rule created as Unconditional has no threshold and blocks every call its filter matches, from the first one — there is no budget to fill. The rules table shows Unconditional or No Limit in the Limit column. See Actions.

A budget window already filled up. Budgets accumulate over their window (per user, per tenant, per feature — whatever the rule applies to). An earlier run in the same window may have spent the allowance; the reason field says so. Note that a sliding window resets only after a quiet gap, so a pool that keeps spending never resets.

A loop rule tripped. Loop detection watches one trace and stops a later call on that trace once it looks stuck — see Loop detection.

The opposite problem: it should block but doesn't

Enforcement takes both dials — the SDK firewall mode and the rule's own mode. If you expected a block and got none, one dial is still on dry-run: check the firewall value in your init(), and the Current Mode column next to the rule in the rules table. The full truth table is in Dry-run vs enforce.

A Notify rule never blocks anything at all — it has no Dry-run/Enforce dial and can't be promoted to one. If you wanted a wall, the action has to be Block.

To stop all blocking immediately without touching your rules, set firewall="off" (Python) / firewall: "off" (Node). Usage keeps flowing; no call can ever be blocked.

Next