Changing a rate limit is a blind edit: raiseDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/AmeyaBorkar/throttlekit/llms.txt
Use this file to discover all available pages before exploring further.
limit from 100 to 150 and you find out who it lets through (or newly blocks) only in production. Policy Plans makes that effect legible before you deploy. Given the policy you run today (current), a policy you’re considering (candidate), and a corpus of recorded arrivals from real traffic, it produces a directional allow↔deny flip ledger — the blast radius of the change — computed entirely off the decision path.
Policy Plans is built on throttlekit/testkit (the deterministic recorder/replayer) and throttlekit/config, adding no frozen-core change. It is @experimental — opt-in, excluded from the 1.x SemVer surface.
Recording Traffic
The first step is to capture real traffic against your current limiters usingrecordLimiter from throttlekit/testkit:
recordLimiter accepts a LimiterSpec — the same shape used by .throttlekit.yaml — and returns a Recording object with:
recording.limiter— aLimiterthat records every synchronous decision. OnlycheckSyncandcheckManySyncare supported; asynccheckandresetare refused (they cannot be captured deterministically).recording.clock— theManualClockdriving the recording. Advance it between checks to simulate real arrival timing.recording.trace()— a snapshot of the immutable trace recorded so far.
Declaring Policy Sets
Policy is a named leaf LimiterSpec plus a ReplayFingerprint that provably rebuilds the exact limiter it describes. A PolicySet is a versioned, content-addressed bag of policies — its contentHash is a SHA-256 over the canonical (sorted) policies, so “did it change?” is a hash comparison.
Building a Corpus
corpusFromRecordings— from liveRecordingobjects (the output ofrecordLimiter).corpusFromTraces— from already-serialized replay traces produced byarrivalsFromTrace.
Running the Plan
plan(current, candidate, corpus) produces a Plan containing a PolicyDiff for every policy name present in both sets, plus a PlanSummary with set-level added / removed policy names.
PolicyDiff Fields
Each per-policy diff carries a PolicyDiffState:
| State | Meaning |
|---|---|
ok | Replayed cleanly; the flip ledger is exact |
empty | No recorded traffic for this policy |
truncated | The corpus was a prefix (the trace hit its recording cap); the ledger covers only the prefix and understates the full effect |
not-replayable | A known non-rate axis (concurrency / escrow / joint-LP); observe live via binding-axis attribution |
refused | A replay precondition was violated (carries a machine-readable ReplayRefusal reason) |
allowToDeny— a tightening (allow→deny): requests the current policy allowed that the candidate would deny (the blast radius)denyToAllow— a loosening: requests the current policy denied that the candidate would allowflippedTotal—allowToDeny + denyToAllowaffectedKeys— number of distinct keys with at least one fliptopFlippedKeys— the top movers (KeyFlip[]), sorted by flip count
CI Gate
assertPlanAcceptable throws PlanRejectedError carrying a machine-readable violation list when any PlanBudget bound is exceeded. Returns silently when within budget. Use this as a CI gate to block deployments that would newly rate-limit more than N requests.
PlanBudget
Maximum new 429s introduced by the candidate. The primary blast-radius guard.
Maximum new allows (loosening). Useful for security-sensitive policies.
Maximum total flips in either direction.
Maximum number of distinct keys with any flip.
Fail if any policy in the plan has
state: "not-replayable" or state: "refused".Full Example
CI Integration Pattern
Add Policy Plans to your CI pipeline to gate every limit change:scripts/run-plan.mjs script reads a saved replay trace (corpusFromTraces), builds the candidate policy from the branch’s .throttlekit.yaml, runs plan, and calls assertPlanAcceptable.
Limitations
Atruncated corpus understates the full effect — it covers only the recorded prefix. It never overstates. Re-record with a higher maxSteps if the corpus is systematically truncated.
FAQ: What does content-addressed mean for PolicySet?
FAQ: What does content-addressed mean for PolicySet?
Each
PolicySet carries a contentHash — a SHA-256 over its canonical (name-sorted, key-sorted) policy list plus the unreplayable policy names. This means “which policy set is deployed?” is a single hash, “did the policy change?” is a hash compare, and a stored PolicySet is integrity-checked on parse. Versioning uses POLICY_SET_FORMAT_VERSION; a stored set with a different version is refused on parse rather than silently misread.FAQ: Can I use plan() without recordLimiter by providing my own arrivals?
FAQ: Can I use plan() without recordLimiter by providing my own arrivals?
Yes. Use
policyCorpus to build a PolicyCorpus from hand-constructed Arrival[] arrays, or corpusFromTraces to read from serialized ReplayTrace JSON. emptyCorpus gives you a no-traffic baseline. The corpusFromRecordings adapter is just the most convenient path for recording from live traffic.