Every ThrottleKit decision is a structured object — observability turns the stream of them into metrics, spans, and aggregates without coupling the library to any vendor SDK. Three layers are available: a raw decision tap for shipping to any sink, built-in in-process analytics with bounded-memory heavy-hitter tracking, and a full OpenTelemetry integration. The ThrottleKit Lens dashboard renders all of this in a terminal UI.Documentation 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.
tapDecisions() — Raw Decision Stream
tapDecisions wraps any Limiter and fires a callback once per completed check (after the decision resolves). The limiter is returned unchanged and all methods — including optional peek / forecast / close — are forwarded.
DecisionEvent
The key that was checked.
The effective cost of the check (default
1).The decision returned to the caller.
The active strategy’s stable name (
"gcra", "tokenBucket", "fixedWindow", etc.).Wall time spent inside the inner check, in fractional milliseconds. For batch checks (
checkMany), this is an equal share per key.Which method produced the event:
"check" | "checkSync" | "checkMany" | "checkManySync".withAnalytics() — Built-In In-Process Analytics
withAnalytics wraps any limiter and tracks allow/deny counts and top-K heavy hitters in-process, on a fixed epoch-aligned window. No OpenTelemetry backend, no external peer, zero configuration.
withAnalytics is @experimental — excluded from the 1.x SemVer guarantee.
AnalyticsOptions
How many heavy hitters each summary tracks. Bounds memory — at most
topK slots regardless of distinct-key cardinality. Default 10.Fixed, epoch-aligned window width in milliseconds. Default
60_000.Injected clock for deterministic tests. Defaults to the system clock.
AnalyticsSnapshot
Epoch-ms of the current window start.
The configured window width.
Requests admitted in the current window.
Requests denied in the current window.
allowed + denied.denied / total, or 0 when total is 0.Keys driving the most requests this window, count-descending. At most
topK entries.Keys driving the most denials this window, count-descending. At most
topK entries.HeavyHitter
topK entries are held regardless of distinct-key cardinality, and it over-estimates only — never drops a true heavy hitter. This is the correct bias for abuse detection.
OpenTelemetry Integration
throttlekit/otel provides a first-class OpenTelemetry integration with a frozen metric/attribute contract — names are pinned by a contract test so a rename never silently breaks dashboards on a patch upgrade.
@opentelemetry/api is imported type-only, so it is erased at compile time — zero runtime dependency is added unless you install and initialize a concrete OTel SDK.
Frozen Metrics
| Name | Type | Labels | Description |
|---|---|---|---|
throttlekit.checks | Counter | strategy, allowed | Total check calls |
throttlekit.remaining | Histogram | strategy | Remaining capacity at each check |
throttlekit.store.latency | Histogram | strategy | Inner check wall-time, ms |
throttlekit.concurrency.limit | Gauge | — | Adaptive concurrency ceiling |
throttlekit.concurrency.inflight | Gauge | — | Current in-flight count |
throttlekit.concurrency.rtt_noload | Gauge | — | Estimated no-load RTT |
Frozen Span Attributes
| Attribute | Description |
|---|---|
throttlekit.strategy | Strategy name |
throttlekit.allowed | true / false |
throttlekit.limit | The limit field of the decision |
throttlekit.remaining | The remaining field |
throttlekit.retry_after_ms | The retryAfterMs field |
throttlekit.binding_axis | For unifiedAdmission denials: "rate" | "concurrency" | "cost" |
Binding-Axis Attribution
throttlekit.binding_axis reports, for a denied unifiedAdmission, which of the three axes (rate / concurrency / cost) actually bound the decision. This is derived by bindingAxisOf(lastDecisions()):
bindingAxisOf is the single source for both the in-band UnifiedAdmission.bindingAxis field and the OTel span attribute — they can never disagree.
ThrottleKit Lens — Terminal Dashboard
Start the ThrottleKit gRPC server with the--tui flag to launch the Lens in-terminal dashboard:
| Tab | What it shows |
|---|---|
| Overview | Allow/deny counts, deny rate, top requested and top denied keys for the current window |
| Latency | p50 / p99 check latency histogram per policy |
| Fairness | Per-tenant weighted share, WFE utilization, guaranteed floor vs actual usage |
| Capacity | Adaptive concurrency ceiling, in-flight count, no-load RTT estimate |
| Guarantee | GALE window-coupled leasing: per-window admitted vs limit, overshoot bound status |
| Cost Room | Token-budget remaining, per-tenant cost burn rate, reservation efficiency |
| Replay | Live denial feed (server-side rate-capped, backpressured — drops on a slow reader) |
| Plan | Policy Plans: last recorded diff between current and candidate policy sets |
Monitor Door — gRPC + Prometheus
The ThrottleKit gRPC server exposes two programmatic read surfaces: gRPC Monitor service (throttlekit.v1.Monitor):
GetSnapshot— returns a typedMonitorMetaenvelope plus per-policy/guard summaries andraw_json(the fullLensSnapshotas JSON). The typed fields are the stable wire contract; evolving internal analytics ride inraw_json.Watch— opens a live, server-side rate-capped and backpressured server-streamed denial feed. A slow reader drops events — it never grows server memory and never blocks the control path.
/metrics (enabled with --metrics-port):
- Exposes aggregate, PII-free series only: per-policy allow/deny, per-axis denials, observed ceiling, p50/p99 latency, guard health.
- No per-key series. This is what lets it default to loopback without auth.
FAQ: Will a tap that throws break my rate limiter?
FAQ: Will a tap that throws break my rate limiter?
No. Every tap callback is wrapped in a try/catch. An exception inside your
onDecision callback is silently dropped and the limiter continues operating normally. Observability must never be able to break the control path.FAQ: Why is withAnalytics marked experimental?
FAQ: Why is withAnalytics marked experimental?
The
analytics() snapshot shape may evolve — new fields, changed semantics for edge cases — in a minor release. The core Limiter interface it wraps is stable, but the analytics surface is not yet covered by the 1.x SemVer guarantee. Use it freely; just expect that resetAnalytics() and the AnalyticsSnapshot fields may change before 1.0.FAQ: How does Space-Saving avoid dropping heavy hitters?
FAQ: How does Space-Saving avoid dropping heavy hitters?
The Space-Saving algorithm (Metwally et al. 2005) maintains at most
topK monitor slots. When a new key arrives and the tracker is full, it evicts the minimum-count slot and gives the new key count = min + 1, inheriting the minimum as an over-estimation bound. This means an entry’s count is always an upper bound on its true frequency — never an undercount. A genuine heavy hitter that has been seen many times will always have a count high enough to survive eviction; only truly light keys can be displaced.