Observability
See what your limiter is doing. Built-in telemetry surfaces every check with rich metadata, a zero-dependency in-process aggregator, and a drop-in OpenTelemetry adapter for production dashboards.
Telemetry hook
One callback per check, with rich metadata: policy, algorithm, key strategy, endpoint, cost, and plan.
StatsCollector
Zero-dep aggregator with snapshot() and reset(). Drop into a /halt/stats endpoint and ship.
OpenTelemetryMetrics
Adapter that emits five OTel counters through an injected meter no hard OTel dependency.
Which one to reach for
They're complementary many teams run both, composed with CompositeTelemetry.
StatsCollector in-process
- • Zero dependencies, drop-in.
- • One JSON endpoint at
/halt/stats. - • Quick debugging, dashboards from a single instance.
- • Caveat: numbers are per-process, not fleet-wide.
OpenTelemetryMetrics fleet-wide
- • Counters land in your existing OTel pipeline.
- • Aggregates across every replica.
- • Plays nicely with Prometheus, Datadog, Honeycomb.
- • Needs OTel SDK + collector wired up.
The telemetry hook
Both SDKs take an optional telemetry argument on the limiter (and on QuotaManager and PenaltyManager). The hook receives one event per rate-limited check with this metadata:
| Field | What it tells you |
|---|---|
policy | The policy name (e.g. public_api). |
algorithm | Token bucket / fixed window / sliding window / leaky bucket. |
keyStrategy / key_strategy | Per-IP, per-user, per-API-key, composite. |
endpoint | Route identifier when the adapter provides one. |
cost | Weighted cost consumed (0 when blocked). |
plan | Optional plan label from Policy.plan falls back to the policy name. |
StatsCollector drop-in aggregator
StatsCollector is a zero-dependency in-process aggregator. Wire it up, then serve snapshot() on a small admin endpoint to read live numbers.
import express from 'express';
import { RateLimiter, StatsCollector, presets } from 'halt-rate';
const stats = new StatsCollector({ topN: 20, maxTrackedKeys: 10_000 });
const limiter = new RateLimiter({
store,
policy: presets.PUBLIC_API,
telemetry: stats,
});
const app = express();
app.get('/halt/stats', (_req, res) => res.json(stats.snapshot()));
// protect this route it exposes top limited keysfrom fastapi import FastAPI
from halt import RateLimiter, StatsCollector, presets
stats = StatsCollector(top_n=20, max_tracked_keys=10_000)
limiter = RateLimiter(
store=store,
policy=presets.PUBLIC_API,
telemetry=stats,
)
app = FastAPI()
@app.get("/halt/stats")
def halt_stats():
return stats.snapshot() # protect this route in productionGET /halt/stats response{
"allowedTotal": 12_348,
"blockedTotal": 217,
"byPolicy": {
"public_api": { "allowed": 11_205, "blocked": 142 },
"auth_endpoints": { "allowed": 843, "blocked": 75 }
},
"byEndpoint": {
"GET /v1/data": { "allowed": 7_412, "blocked": 88, "cost": 7_412 },
"POST /v1/login": { "allowed": 843, "blocked": 75, "cost": 843 }
},
"topLimitedKeys": [
{ "key": "ip:203.0.113.42", "blocked": 64 },
{ "key": "user:42", "blocked": 23 }
],
"costByPlan": { "free": 4_120, "pro": 8_228 },
"quotaExceeded": 5,
"penaltiesApplied": 1,
"violations": 72,
"trackedKeys": 934
}Field names are camelCase in TypeScript and snake_case in Python the shape is identical.
Caveats worth knowing
- Per-process. StatsCollector is in-memory. Run it on each instance and aggregate across the fleet with OpenTelemetry / Prometheus (below).
- Bounded memory. The tracked-key table is capped at
maxTrackedKeys; when it fills, the smallest counters are evicted before a new key is added. - Cost is only consumed. Blocked requests don't contribute to
costonly allowed ones do.
OpenTelemetry metrics
OpenTelemetryMetrics emits these five counters through a meter you inject:
halt.requestsEvery check, tagged with policy / algorithm / plan / outcome.
halt.blockedOnly blocked checks easy alerting on a rising rejection rate.
halt.costSum of consumed cost for weighted endpoints.
halt.quota.exceededQuota rejections from QuotaManager.
halt.penalty.appliedPenalty events from PenaltyManager.
npm install @opentelemetry/apiimport { metrics } from '@opentelemetry/api';
import { RateLimiter, OpenTelemetryMetrics, presets } from 'halt-rate';
const telemetry = new OpenTelemetryMetrics(metrics.getMeter('halt'));
const limiter = new RateLimiter({
store,
policy: presets.PUBLIC_API,
telemetry,
});pip install "halt-rate[otel]"from opentelemetry import metrics
from halt import RateLimiter, OpenTelemetryMetrics, presets
# Inject your meter, or omit to auto-acquire from the global MeterProvider.
telemetry = OpenTelemetryMetrics(meter=metrics.get_meter("halt"))
limiter = RateLimiter(
store=store,
policy=presets.PUBLIC_API,
telemetry=telemetry,
)Composing hooks
Use CompositeTelemetry to fan out to multiple hooks local StatsCollector for a quick /halt/stats endpoint and OpenTelemetry for fleet-wide dashboards.
import { metrics } from '@opentelemetry/api';
import {
CompositeTelemetry,
StatsCollector,
OpenTelemetryMetrics,
RateLimiter,
} from 'halt-rate';
const telemetry = new CompositeTelemetry([
new StatsCollector({ topN: 20 }),
new OpenTelemetryMetrics(metrics.getMeter('halt')),
]);
const limiter = new RateLimiter({ store, policy, telemetry });from opentelemetry import metrics
from halt import (
CompositeTelemetry,
StatsCollector,
OpenTelemetryMetrics,
RateLimiter,
)
telemetry = CompositeTelemetry([
StatsCollector(top_n=20),
OpenTelemetryMetrics(meter=metrics.get_meter("halt")),
])
limiter = RateLimiter(store=store, policy=policy, telemetry=telemetry)Per-plan tagging
Set an optional plan label on a policy and Halt propagates it through every telemetry event handy for splitting metrics by tier. The built-in PLAN_* presets already set it (free / starter / pro / business / enterprise); for custom policies, set it yourself:
const policy = {
name: 'pro_api',
plan: 'pro', // tag for telemetry
limit: 10_000,
window: 3600,
algorithm: Algorithm.TOKEN_BUCKET,
};policy = Policy(
name="pro_api",
plan="pro", # tag for telemetry
limit=10_000,
window=3600,
algorithm=Algorithm.TOKEN_BUCKET,
)Quotas and penalties
QuotaManager and PenaltyManager both accept the same telemetry argument and emit dedicated events (quota_check, quota_exceeded, penalty_applied, violation). Pass the same StatsCollector instance to all three for unified counters.
Metrics vs spans
Telemetry (this page) is about metrics counts and totals you can chart. For per-request spans in your distributed tracing tool, use the limiter's separate otelTracer / otel_tracer argument. They're independent wire up either, both, or neither.