Strategy Statistics¶
Sometimes you want to look at the rate limit counter without actually touching it. Maybe you want to add X-RateLimit-Remaining headers to every response. Maybe you want a /usage endpoint that tells clients where they stand. Maybe you want to feed data into Prometheus.
That's what throttle.stat(...) is for.
stat() is read-only
Calling stat(...) never consumes quota. It reads the current state from the backend and returns it. Your clients can call a stats endpoint as often as they like — it won't move the rate limit needle.
Basic Usage¶
stat(...) returns a StrategyStat object, or None if the strategy doesn't support stats (custom strategies) or if the client is exempt from throttling.
from fastapi import FastAPI, Request, Depends
from traffik import HTTPThrottle
from traffik.backends.redis import RedisBackend
backend = RedisBackend("redis://localhost:6379", namespace="myapp")
app = FastAPI(lifespan=backend.lifespan)
throttle = HTTPThrottle("api:read", rate="100/min", backend=backend)
@app.get("/items")
async def get_items(request: Request = Depends(throttle)):
stat = await throttle.stat(request)
if stat:
print(f"Hits remaining: {stat.hits_remaining}")
print(f"Wait time: {stat.wait_ms}ms")
return {"items": [...]}
StrategyStat Fields¶
| Field | Type | Description |
|---|---|---|
key |
Stringable |
The full namespaced throttle key for this client |
rate |
Rate |
The rate limit definition (e.g., 100 requests/minute) |
hits_remaining |
float |
Quota left in the current window. Can be inf for unlimited. |
wait_ms |
float |
Milliseconds until the client can send again (0 if not throttled) |
metadata |
TypedDict \| None |
Strategy-specific data (window timestamps, token counts, etc.) |
Typed Metadata Per Strategy¶
Each built-in strategy exposes rich metadata you can use for observability.
from traffik.strategies.fixed_window import FixedWindowStatMetadata
stat = await throttle.stat(request)
if stat and stat.metadata:
meta: FixedWindowStatMetadata = stat.metadata
# meta["strategy"] -> "fixed_window"
# meta["window_start_ms"] -> window start (Unix ms)
# meta["window_end_ms"] -> window end (Unix ms)
# meta["current_count"] -> requests so far in this window
from traffik.strategies.sliding_window import SlidingWindowLogStatMetadata
stat = await throttle.stat(request)
if stat and stat.metadata:
meta: SlidingWindowLogStatMetadata = stat.metadata
# meta["strategy"] -> "sliding_window_log"
# meta["window_start_ms"] -> rolling window start
# meta["entry_count"] -> number of log entries in window
# meta["current_cost_sum"] -> total cost in window
# meta["oldest_entry_ms"] -> timestamp of oldest entry
from traffik.strategies.sliding_window import SlidingWindowCounterStatMetadata
stat = await throttle.stat(request)
if stat and stat.metadata:
meta: SlidingWindowCounterStatMetadata = stat.metadata
# meta["strategy"] -> "sliding_window_counter"
# meta["current_window_id"] -> current window identifier
from traffik.strategies.token_bucket import TokenBucketStatMetadata
stat = await throttle.stat(request)
if stat and stat.metadata:
meta: TokenBucketStatMetadata = stat.metadata
# meta["strategy"] -> "token_bucket"
# meta["tokens"] -> current token count
# meta["capacity"] -> bucket capacity (burst size)
# meta["refill_rate_per_ms"] -> tokens added per millisecond
Adding Rate Limit Headers to Responses¶
The most common use of stats is adding standard X-RateLimit-* headers so clients know where they stand.
from fastapi import FastAPI, Request, Response, Depends
from traffik import HTTPThrottle
from traffik.backends.redis import RedisBackend
import math
backend = RedisBackend("redis://localhost:6379", namespace="myapp")
app = FastAPI(lifespan=backend.lifespan)
throttle = HTTPThrottle("api", rate="100/min", backend=backend)
@app.get("/items")
async def get_items(
request: Request = Depends(throttle),
response: Response = None,
):
stat = await throttle.stat(request)
if stat:
response.headers["X-RateLimit-Limit"] = str(int(stat.rate.limit))
response.headers["X-RateLimit-Remaining"] = str(max(int(stat.hits_remaining), 0))
if stat.wait_ms > 0:
retry_after = math.ceil(stat.wait_ms / 1000)
response.headers["Retry-After"] = str(retry_after)
return {"items": [...]}
Built-in header support
You can also configure headers directly on the throttle using the headers parameter and the Header API — Traffik will resolve them automatically on each throttled response. See the Headers reference for details.
Building a /usage Endpoint¶
Give your clients a dedicated endpoint to check their quota without making a real request:
from fastapi import FastAPI, Request
from traffik import HTTPThrottle
from traffik.backends.redis import RedisBackend
backend = RedisBackend("redis://localhost:6379", namespace="myapp")
app = FastAPI(lifespan=backend.lifespan)
api_throttle = HTTPThrottle("api:standard", rate="1000/hour", backend=backend)
@app.get("/usage")
async def get_usage(request: Request):
"""Returns quota info without consuming quota."""
stat = await api_throttle.stat(request)
if stat is None:
return {"message": "No rate limit info available (you may be exempt)"}
return {
"limit": int(stat.rate.limit),
"remaining": max(int(stat.hits_remaining), 0),
"reset_ms": stat.wait_ms,
"window_ms": stat.rate.expire,
}
Clients can hit /usage as often as they like — it reads the counter but never writes to it.
Prometheus Metrics Example¶
If you're exporting metrics to Prometheus, stats give you everything you need:
from prometheus_client import Gauge, Counter
from fastapi import FastAPI, Request, Depends
from traffik import HTTPThrottle
from traffik.backends.redis import RedisBackend
backend = RedisBackend("redis://localhost:6379", namespace="myapp")
app = FastAPI(lifespan=backend.lifespan)
throttle = HTTPThrottle("api", rate="1000/hour", backend=backend)
quota_remaining = Gauge(
"traffik_quota_remaining",
"Quota remaining for the current window",
["throttle_uid"],
)
throttle_hits = Counter(
"traffik_requests_total",
"Total requests processed by throttle",
["throttle_uid"],
)
@app.middleware("http")
async def collect_throttle_metrics(request: Request, call_next):
response = await call_next(request)
stat = await throttle.stat(request)
if stat:
quota_remaining.labels(throttle_uid="api").set(stat.hits_remaining)
throttle_hits.labels(throttle_uid="api").inc()
return response
Avoid calling stat(...) in tight loops
Each stat(...) call hits the backend. In a middleware that runs on every request, that's fine — it's one extra read per request. But calling it in a loop for many keys at once is a different story. Batch them if you need to.
Summary¶
stat(...)is your window into the rate limiter's current state — without modifying it- Returns
Nonefor exempt clients and strategies without stat support - Use it for response headers, usage endpoints, dashboards, and metrics
- All built-in strategies support
stat(...)with typed metadata