A performance sweep, then a hardening pass: 1.6.1 to 1.7.0
Two back-to-back releases across all three packages: a measured performance sweep (throttlekit 1.6.1, server 0.4.4, py 0.5.1), followed one day later by a fuzz-and-mutation hardening pass that closed a non-finite-decision bug class (throttlekit 1.7.0).
Two releases landed back to back. On June 30th, a measured performance sweep across all
three packages. On July 1st, a hardening pass on the core that closed a class of boundary
bugs a new fuzz harness turned up. Neither touches the frozen 1.x API or the wire. Here’s
what shipped in each.
throttlekit 1.6.1 — the performance sweep
A multi-agent audit ran across the core, the server, and the Python client: finders proposed
optimizations by dimension (allocation, hashing, round trips, and so on), then every finding
went through an adversarial pass for hotness, impact, and safety before it was allowed to
land. 33 opportunities came out of that, 29 confirmed and 4 dismissed as real but negligible
(the kind of inefficiency that only shows up on a cold path or over the network). The
already-hot gcra checkSync path (~170 ns) got zero findings, which is the right outcome:
it was already allocation-tight.
Most of the audit’s real targets turned out to sit on paths with no benchmark at all yet: the
multi-dimension combine, the two-tier weighted-fair-escrow grant, the server’s Check
handler. So the first step wasn’t optimizing, it was writing benchmarks for those paths and
wiring them into the CI regression gate. That’s what made the wins provable, and what keeps
them from regressing later. Same machine, median of three runs, before vs. after:
| Path | Before | After | Δ |
|---|---|---|---|
multiRateLimit.checkSync (2-dim) | 4968 ns | 2968 ns | −40% |
multiRateLimit.checkSync (3-dim) | 7507 ns | 4599 ns | −39% |
weightedFairEscrow grant (8 tenants) | 182 ns | 155 ns | −15% |
server RateLimiter.Check handler | 898 ns (1.11M/s) | 675 ns (1.48M/s) | −25% |
The core single-strategy checkSync paths (gcra, tokenBucket, fixedWindow) are flat within
noise on purpose: they were already gated and tight, so nothing there needed touching.
The hero fix was almost embarrassing once found. The multi-dimension sync path was doing a
deep structuredClone of every dimension’s stored state on every decision, to protect the
no-partial-consume contract, even when that state was an immutable primitive (which is
exactly GCRA’s case). Gating the clone on object-ness, so primitives skip it and only
object/array states still clone, cut about 40% off the common per-IP-and-per-user composite
case with byte-identical decisions. Three independent finders converged on the same fix.
The one with a real trap was the weighted-fair-escrow aggregate. Swapping an O(active-tenants) rescan for a running Σweight/Σused counter is only safe for integer weights and costs: a mutation-order float sum can 1-ULP-diverge from a fresh rescan and flip a floor decision. So the fast path only fires on integer input and falls back to the exact rescan the instant a fractional value shows up. A new conformance test drives fractional adversarial timelines and also proves the integer gate itself is load-bearing, meaning the test fails if someone quietly removes it.
Everything here is an internal optimization. Decisions, the frozen 1.x API, and the golden
wire vectors are all unchanged. A few candidate optimizations were declined too: some were
high-risk for low reward, others were blocked by the frozen, checksummed wire scripts. Optimize
as much as possible, but not at the cost of the guarantees.
throttlekit-server 0.4.4
The server’s share of the sweep is the Check handler line above (898 ns → 675 ns, −25%,
1.11M → 1.48M ops/s), part of the same measured pass.
throttlekit-py 0.5.1
The Python client picked up its own slice of the sweep: slots=True on the per-call frozen
dataclasses (Decision, Forecast, Lease, LeaseGrant) roughly halves a transported
decision’s footprint, from 160 to 72 bytes. The Redis backend now precomputes its ARGV
template once per backend instead of rebuilding it on every call. A handful of smaller
inlining cleanups round it out.
In total, 18 optimizations landed across the three repos, each as a small, byte-identical, individually tested commit.
throttlekit 1.7.0 — the hardening pass (core only)
After the perf sweep, the next question was how to make the core fail loud instead of fail
weird. A new bug-surfacing layer went in: Stryker mutation testing on the core decision math
(a survivor campaign lifted the score from 65.4% to 77.5%, behind a regression floor),
model-based fc.commands suites over composite and sequencing behavior, a live-store CI gate
(Redis, Postgres, and DynamoDB) running on every push, and fast-check fuzz harnesses on the
untrusted-input boundaries: the config parser, IP/XFF parsing, key/cost handling, and sketch
merge. Mutation testing and model-based testing found no product bugs (the earlier audit’s
fixes held). The fuzzers found five real issues, F1 through F5, all now guarded.
The finding that generalized the furthest: a subnormal gcra limit made an internal
derived time value (period / limit) evaluate to Infinity, which produces a non-finite
resetAt / retryAfterMs, which becomes a malformed RateLimit-Reset HTTP header. Guarding
only gcra would have been a trap, because the same shape (a construction parameter
overflowing derived time arithmetic) exists in every sibling strategy that divides by a rate
or a period. So instead of a one-off patch, a single executable invariant test drives each
strategy through adversarial construction parameters and asserts it either throws a
RangeError or only ever emits finite decisions. That turned the fix into a sweep, and it
caught two more cases along the way: a finite but astronomical emission interval could still
overflow the accumulating internal state after just two requests, so the real bound has to be
a safe-integer count of milliseconds, not merely “finite.” And the worst case was
leakyBucket, where a non-finite computed delay made its schedule() call sleep(Infinity)
and simply never fire: a silent hang, not a bad number.
The rest of the boundary hardening:
- The zero-dependency YAML/JSON config parser now caps block-nesting depth at 64. A deeply
nested untrusted
.throttlekit.yamlcould previously stack-overflow the process, a denial of service on untrusted config text. - A per-request
costaboveNumber.MAX_SAFE_INTEGERis now rejected. It previously overflowed internal cost arithmetic to a non-finite retry value. slidingWindow.bucketsis now capped at 100,000. An unbounded value was a memory amplification vector, since the estimator holds an O(buckets) ring per key.- A decoded DDoS-sketch snapshot with a poisoned or non-finite total count is now rejected on decode and ignored on merge, protecting the cluster-wide count used for shed decisions from a malformed peer snapshot.
This is a minor release, not a patch, for one specific reason: a handful of previously
“working” (silently garbage-returning) absurd option values now throw a RangeError instead.
No valid input’s behavior changed, but that’s still a real behavior change worth flagging.
Status: the storeless core test suite is at 1551 tests green, lint and strict types are
clean, and package surface checks (publint, attw) are clean.
Full per-release detail lives in each package’s CHANGELOG.md, linked from the
changelog; current shipped versions are on the status board. None of
this touches the frozen 1.x API or the gRPC wire.