Skip to content

[Feature]: Distributed rate-limiter algorithm backed by Valkey #2241

Description

@daric93

This is a?

New feature

Detailed description

Summary

Add a valkey_fixwin rate-limiting algorithm that provides distributed, cross-worker fixed-window counting backed by Valkey. It would register through the existing @algo.algo_class registry alongside the current in-memory fixwin algorithm (which stays the default) and implement the existing ReteLimitAlgo ABC (require_access / release_access) — no new interface. Cross-worker counting is done with atomic INCR + EXPIRE via a small Lua script.

Motivation

LangBot's current rate limiter (pipeline/ratelimit/algos/fixedwin.py) keeps its counters in process memory (dict + asyncio.Lock). That works for a single-process deployment, but when LangBot is scaled horizontally to multiple workers, each process keeps its own independent counters. The effective limit becomes configured_limit × number_of_workers, so the configured cap is not enforced correctly across the deployment.

Operators running LangBot in toB/SaaS or otherwise scaled setups need limits enforced consistently regardless of which worker handles a message. A shared counter store solves this directly, and Valkey — the open-source, Linux Foundation–backed fork of Redis — is a natural fit since many such deployments already run it. Making the algorithm pluggable and opt-in keeps single-process users completely unaffected.

Proposed Implementation

  • New src/langbot/pkg/pipeline/ratelimit/algos/valkey_fixwin.py with a ValkeyFixedWindowAlgo class decorated @algo.algo_class('valkey_fixwin'), implementing the existing ReteLimitAlgo ABC and mirroring the window semantics of the in-memory fixwin.
  • Atomic cross-worker fixed-window counting: per (launcher_type, launcher_id, window_start) key, a single Lua script does check-then-INCR and sets EXPIRE on first increment, so the window reaps itself via TTL and the increment is atomic across workers.
  • Make the algorithm config-selectable via safety.rate-limit.algo. Today pipeline/ratelimit/ratelimit.py hardcodes algo_name = 'fixwin'; this would read the algo from config and default to 'fixwin' (backward-compatible). Add the selector to the safety.yaml pipeline metadata (with en_US + zh_Hans labels) and an "algo": "fixwin" default to the default pipeline config.
  • A top-level valkey: config block (host/port/db/username/password/tls) for the shared connection. Reuses the valkey-glide client convention (client_name, lazy_connect=True, optional auth/TLS).
  • Opt-in / disabled by default: fixwin (in-memory) remains the default; valkey_fixwin is inert unless explicitly selected.
  • Fail-open: if Valkey is unreachable, the limiter logs a throttled warning and allows the request rather than dropping legitimate traffic — a rate limiter is a safety throttle, not an auth gate. (A fail-closed toggle could be added later.)

Dependencies

  • valkey-glide>=2.4.1,<3.0.0 (optional dependency; official async-native Valkey client with reconnect/backoff, TLS, lazy-connect). License is permissive (Apache-2.0 / BSD).
  • A Valkey server for deployments that opt into the distributed algorithm (e.g. the valkey/valkey-bundle:9.1.0 image).

Testing Plan

  • Unit tests (mocked glide client, no server): key schema + window math, allow within limit, deny over limit (drop), wait-strategy retry into the next window, fail-open on client error, client-config build (client_name / lazy_connect / auth / TLS), and registry membership. The existing in-memory fixwin unit tests are retained as a regression guard, plus algo-selection tests (default stays fixwin, unknown algo raises).
  • Integration tests against a live valkey/valkey-bundle:9.1.0 instance, including a multi-worker shared-counter test: two separate algorithm instances with their own clients (simulating two worker processes) point at the same Valkey key; together they may only consume the configured limit, and one more request from either instance is denied — proving cross-process limiting. These follow the existing slow-gated / env-gated test precedent (same pattern as the Postgres migration tests).

I'd be happy to contribute this as a PR if it aligns with the project roadmap. It's purely additive, keeps the in-memory fixwin as the default, and reuses the existing algorithm registry and ReteLimitAlgo ABC. The one cross-cutting change to flag is making ratelimit.py read the algorithm from config instead of hardcoding 'fixwin' (backward-compatible via the default).

Metadata

Metadata

Assignees

No one assigned

    Labels

    eh: Featureenhance: 新功能添加 / add new features

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions