Policy Plans — a terraform plan for your limits
Replay your own recorded traffic against a candidate config and read the exact per-policy allow↔deny diff before you deploy. Shipped in throttlekit 1.4.0 and gate-able in CI.
Changing a rate limit in production is a guess. You tighten a window from 100 to 60, ship
it, and find out who you broke from the support queue. There’s no terraform plan for
limits: no way to see the blast radius before it lands.
Now there is. Policy Plans shipped in throttlekit@1.4.0
(the throttlekit/policy subpath):
import { plan, renderPlan } from "throttlekit/policy";
const result = plan(current, candidate, corpus); // pure, never throws
console.log(renderPlan(result));
You give it your current policy set, a candidate, and a corpus of recorded arrivals. It cold-replays the current policy over those arrivals to derive a baseline, replays the candidate over the same arrivals, and returns a directional flip ledger. Per policy, per key, it shows exactly which requests would flip allow→deny or deny→allow, plus the top movers.
Honest by construction
The number is only useful if it’s truthful, so two lines are drawn hard:
- The baseline is a cold replay over your arrival timing, not warm production. A cold replay can’t reproduce production’s exact in-flight state, and we won’t pretend it can. The diff is candidate-vs-deterministic-baseline over the recorded traffic.
- Only what’s replayable is scored. Leaf rate and cost limiters diff exactly.
Every other axis (concurrency, two-tier escrow, the joint-LP policy) is reported
not-replayable(“observe live via binding-axis attribution”), never a fabricated zero that reads as “no impact.”
Gate it in CI
The whole thing is a pure function built on throttlekit/testkit, with no change to the
frozen 1.x core. That makes it safe to drop into a pipeline:
assertPlanAcceptable(result, { maxDenyFlips: 50, maxKeysAffected: 10 });
…or, from the server, as a fail-closed audited subcommand reading your durable capture store:
throttlekit-server policy plan \
--config ./current.yaml --candidate ./candidate.yaml \
--from-capture --max-allow-deny 0 --max-flips 200
It exits non-zero when the predicted blast radius is exceeded. And in the live --tui
dashboard, press P to diff a candidate against the running config over the deterministic
capture shadow: the same engine, off the decision path.
Stop guessing at limit changes. Plan them.