-
Notifications
You must be signed in to change notification settings - Fork 2
systems rate limiting
Active contributors: Magnus Hedemark
Multi-layer rate limiting with backpressure propagation. Three strategies (LocalTokenBucket, ValkeySlidingWindow, ExternalSidecar) serve per-engine rate limiting, while a separate per-client rate limiter protects the server from noisy tenants. Fail-closed mode with graceful fallback prevents unbounded traffic during Valkey outages.
| Type | File | Description |
|---|---|---|
RateLimitStrategy |
slopsearx/ratelimit.py |
Abstract strategy interface. Subclasses implement acquire(engine, cost) which returns True if the request is allowed. Also provides optional warmup() and shutdown() lifecycle hooks. |
LocalTokenBucket |
slopsearx/ratelimit.py |
In-memory token bucket using time.monotonic() for refill timing. Suitable for development with 1-3 replicas. Not suitable for 50+ replicas because each replica maintains independent state. |
ValkeySlidingWindow |
slopsearx/ratelimit.py |
Distributed sliding window using Valkey INCR and EXPIRE. Correct for all replica counts. Supports fail-closed mode with local fallback after a grace period. |
ExternalSidecar |
slopsearx/ratelimit.py |
Delegates rate limiting to a dedicated external service. Currently a stub that always allows. Intended for advanced deployments where rate limiting is managed by a separate infrastructure component. |
RateLimiter |
slopsearx/ratelimit.py |
Backpressure wrapper around a strategy. Adds 30-second cooldown after rate-limit denial and 3-strike deactivation of engines. |
_EngineState |
slopsearx/ratelimit.py |
Per-engine backpressure tracking dataclass. Fields: consecutive_failures, cooldown_until, deactivated. |
Every adapter calls self.rate_limiter.acquire(engine_name) before sending a request. The RateLimiter delegates to the configured strategy, which decides allow or deny.
For development, the local token bucket maintains per-engine token counts in memory. Each replica maintains its own token count. At 50+ replicas, the effective rate would be 50x the configured rate.
For production, the Valkey sliding window uses a shared counter:
ratelimit:{engine}:{window_start} → INCR + EXPIRE
All replicas atomically increment the same counter. If the result exceeds the configured per-window rate, the request is denied. Keys expire after two window durations.
The server also enforces a per-client request budget using a separate ValkeySlidingWindow instance keyed on request.client.host:
ratelimit:client:{client_ip}:{window_start} → INCR + EXPIRE
Configurable via PER_CLIENT_REQUESTS (default 30 requests) and PER_CLIENT_WINDOW_SECONDS (default 60 seconds). When a client exceeds its budget, the server returns HTTP 429 before any engine dispatch occurs.
The ValkeySlidingWindow supports a fail-closed mode (FAIL_CLOSED=true) for security-sensitive deployments:
- Default (fail-open): When Valkey is unreachable, all requests are allowed through. This prioritizes availability over strict enforcement.
-
Fail-closed: When Valkey is unreachable, all requests are denied during a configurable grace period (
FAIL_CLOSED_GRACE_SECONDS, default 30s). After the grace period expires, the system falls back to an in-processLocalTokenBucketwith the same configured rate so service can continue with approximate enforcement.
This progression (deny → local fallback) prevents unbounded traffic during transient Valkey outages while avoiding permanent denial of service during extended outages.
The RateLimiter wraps a strategy and adds backpressure behavior:
- 30-second cooldown: When a strategy denies a request, the engine enters a 30-second cooldown. During cooldown, all requests to that engine are denied without consulting the strategy.
-
3-strike deactivation: If an engine receives three consecutive rate-limit denials, it is deactivated. Deactivated engines are skipped entirely until a health check passes and
reactivate()is called. -
Automatic recovery: On a successful
acquire()call, the consecutive failure counter resets to zero, and the cooldown is lifted.
-
Server startup:
startup()inserver.pycreates two rate limiters: aRateLimiterwithLocalTokenBucketfor per-engine backpressure (injected into each adapter), and aValkeySlidingWindowfor per-client rate limiting (checked in the search handler before dispatch) -
Adapter calls: Each adapter calls
self.rate_limiter.acquire(self.name)before sending HTTP requests -
Health checks: After a deactivated engine passes a health check, the server calls
_rate_limiter.reactivate(engine_name) -
Fail-closed recovery:
ValkeySlidingWindowattempts reconnection on eachacquire()call and clears the disconnected state on success -
Shutdown: The server calls
_rate_limiter.shutdown()and_client_rate_window.shutdown()during graceful shutdown
- Changing the production strategy: modify server startup to use
ValkeySlidingWindowinstead ofLocalTokenBucketfor per-engine limiting - Adjusting backpressure parameters: modify cooldown duration or strike count in
RateLimiter.acquire() - Changing per-client limits: set
PER_CLIENT_REQUESTSandPER_CLIENT_WINDOW_SECONDSenv vars - Enabling fail-closed: set
FAIL_CLOSED=trueand optionallyFAIL_CLOSED_GRACE_SECONDS - Adding a new strategy: subclass
RateLimitStrategyand implementacquire()
| File | Description |
|---|---|
slopsearx/ratelimit.py |
All rate-limiting strategies (LocalTokenBucket, ValkeySlidingWindow, ExternalSidecar), RateLimiter backpressure wrapper, and _EngineState tracking |