New in v0.3.0

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:

FieldWhat it tells you
policyThe policy name (e.g. public_api).
algorithmToken bucket / fixed window / sliding window / leaky bucket.
keyStrategy / key_strategyPer-IP, per-user, per-API-key, composite.
endpointRoute identifier when the adapter provides one.
costWeighted cost consumed (0 when blocked).
planOptional 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.

TypeScripttypescript
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 keys
Pythonpython
from 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 production
Sample GET /halt/stats response
snapshot.json
{
  "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 cost only allowed ones do.

OpenTelemetry metrics

OpenTelemetryMetrics emits these five counters through a meter you inject:

halt.requests

Every check, tagged with policy / algorithm / plan / outcome.

halt.blocked

Only blocked checks easy alerting on a rising rejection rate.

halt.cost

Sum of consumed cost for weighted endpoints.

halt.quota.exceeded

Quota rejections from QuotaManager.

halt.penalty.applied

Penalty events from PenaltyManager.

TypeScriptbash
npm install @opentelemetry/api
import { 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,
});
Pythonbash
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.

TypeScripttypescript
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 });
Pythonpython
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:

TypeScripttypescript
const policy = {
  name: 'pro_api',
  plan: 'pro',          // tag for telemetry
  limit: 10_000,
  window: 3600,
  algorithm: Algorithm.TOKEN_BUCKET,
};
Pythonpython
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.