throttlekit
·

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.