Expand description
Fixed-window rate limiter as a ServerInterceptor.
The distinction from a token bucket is not cosmetic and this line used to
get it wrong: a fixed window admits up to 2 × requests_per_window across a
window boundary — the tail of one window and the head of the next — where a
token bucket would not. Anyone sizing a limit against an upstream’s hard
ceiling needs to read that from the summary, not discover it further down.
Provides RateLimitInterceptor, a ready-made interceptor that limits
request throughput per caller. The caller key is derived from
CallContext::caller_identity; for unauthenticated callers behind a
trusted reverse proxy, the client IP can be taken from x-forwarded-for
(see RateLimitConfig::trusted_proxy_hops).
§Example
use std::sync::Arc;
use a2a_protocol_server::rate_limit::{RateLimitInterceptor, RateLimitConfig};
let limiter = Arc::new(
RateLimitInterceptor::new(RateLimitConfig {
requests_per_window: 100,
window_secs: 60,
..RateLimitConfig::default()
})
.expect("valid rate limit config"),
);Then add it to the handler builder:
let handler = RequestHandlerBuilder::new(executor)
.with_interceptor(limiter)
.build()?;§Caller identity
The per-caller key is derived in this order:
-
CallContext::caller_identity— set by an authentication interceptor. This is the recommended source: it cannot be forged by the client.Register the authentication interceptor before this one. The chain runs interceptors in registration order over one
CallContext, so a limiter registered first reads an identity nothing has set yet and buckets every caller together. Nothing rejects that ordering; it just stops being per-caller.JwtAuthInterceptor(theauth-jwtfeature) records the validatedsub;ApiKeyAuthInterceptorandBearerTokenAuthInterceptorrecord a label when built withwith_labelled_keys/with_labelled_tokens. The credential is never the key: caller keys reach a shared rate-limit table, logs and metrics, and a secret belongs in none of those. -
The client IP from
x-forwarded-for, only whenRateLimitConfig::trusted_proxy_hopsis non-zero. The header is client-controlled, so by default (trusted_proxy_hops == 0) it is ignored entirely — otherwise a caller could evade the limit by forging a fresh address on every request. -
A shared
"anonymous"key. All remaining callers share one budget, which keeps the limit enforceable (fail-closed) at the cost of granularity.
§Design
Uses a fixed-window counter per caller key. Windows are aligned to wall
clock seconds. When a request exceeds the per-window limit, the before
hook returns an error. A2A / JSON-RPC define no dedicated throttling code,
so this surfaces as an internal error (-32603) whose message names the
rate limit; the request is rejected. (If you need a distinct client-visible
signal for backoff, wrap this in a transport adapter that maps the message
to your preferred status — e.g. HTTP 429.)
The bucket map is bounded by RateLimitConfig::max_buckets. When the
map is full and stale buckets cannot be evicted, requests from new
callers are rejected until capacity frees up (fail-closed).
For production deployments requiring sliding windows, distributed counters,
or more sophisticated algorithms, implement a custom ServerInterceptor
or use a reverse proxy (nginx, Envoy).
Structs§
- Postgres
Rate Limit Counter - A
RateLimitCounterbacked by aPostgreSQLtable. - Rate
Limit Config - Configuration for
RateLimitInterceptor. - Rate
Limit Interceptor - A fixed-window rate limiting
ServerInterceptor.
Constants§
- DEFAULT_
MAX_ BUCKETS - Default cap on the number of tracked caller buckets.
Traits§
- Rate
Limit Counter - A request counter every replica shares.