Skip to content

feat(token_rate_limit): sliding-window/token-bucket rate limiting (M1/M2/M6) - #796

Merged
shaneutt merged 5 commits into
praxis-proxy:mainfrom
jordigilh:jordigilh/token-rate-limit-per-app-budgets
Aug 31, 2026
Merged

feat(token_rate_limit): sliding-window/token-bucket rate limiting (M1/M2/M6)#796
shaneutt merged 5 commits into
praxis-proxy:mainfrom
jordigilh:jordigilh/token-rate-limit-per-app-budgets

Conversation

@jordigilh

@jordigilh jordigilh commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the uncontested MVP core of the token rate limiting proposal (00121_token-rate-limiting.md in praxis-proxy/enhancements, epic ai#121):

  • M1 — ordered rules:, each an optional static header-value match condition bound to its own budget (catch-all default rule when no match: is given)
  • M2 — reservation-based admission: reserve an estimated token cost at request time, reconcile against actual provider-reported usage (token_count's token.total) once the response completes
  • M6 — 429 responses with X-RateLimit-*-Tokens headers on hard deny

Two admission algorithms, chosen per rule via algorithm: (sliding_window | token_bucket, per the maintainer framing on ai#789/praxis#551 that this is "a per-rule choice, similar to shadow/enforcement-action knobs elsewhere"), both behind a pluggable backend: in-process by default, or a shared Valkey backend (backend: {kind: valkey}) for state shared across gateway instances/replicas.

Descoped from this PR: M5 (per-header/per-app bucket keys, ai#129) has been pulled out. It conflicted with the quota-key design ai#790 already established for the Valkey-backed shared quota work (namespace + rule + principal + canonical model, not an arbitrary header value), and ai#790 itself isn't approved yet. Every rule now resolves to a single shared budget regardless of request headers; ai#129 tracks per-key budgets separately pending the quota-key question being resolved with ai#790's stakeholders.

Also deliberately deferred (see filters/src/token_rate_limit/mod.rs module doc for the full list and rationale): configurable estimation (M3), token-type-aware weighting (M4), multiple budgets/soft-limit tiers per rule, observability, and metering.

Additional fixes included

  • Valkey backend now reuses a cached multiplexed connection instead of opening a fresh one per reserve/reconcile call.
  • Fixed a doc-generator bug where token_rate_limit's AlgorithmConfig enum collided with an unrelated struct of the same name in a sibling filter, producing phantom fields in generated docs; renamed to RuleAlgorithm. Also fixed the generator leaking Rust's r# raw-identifier prefix into the match field name.

Testing

  • 113 unit tests in filters/src/token_rate_limit/, covering both algorithms, both backends, config validation, and reservation/reconciliation edge cases (expired-reservation reaping, lost-request handling, capacity denials on all three configured bounds).
  • Integration test exercising the full HTTP round trip against both example configs (examples/configs/token-rate-limit.yaml, examples/configs/token-rate-limit-mixed-algorithms.yaml).
  • make lint (clippy, fmt, cargo doc -D warnings, separator/filter-doc/example-test xtask lints) passes clean.

Also fixed: every Valkey-backed test is gated on TOKEN_RATE_LIMIT_VALKEY_URL, but CI never set it, so they silently no-opped instead of running. Added .github/workflows/valkey.yaml (same shape as postgres.yaml) to run them for real, which surfaced a few more gaps now closed too: ValkeyEval's invalidate-on-failure path, the in-process backends' unused reconcile/enqueue_reconcile trait methods, and two no-op paths in mod.rs.

Supersedes the closed draft #773, which called out the sliding-window-vs-token-bucket question as unresolved; this PR resolves it by supporting both, per rule.

@jordigilh
jordigilh requested review from a team and jland-redhat August 20, 2026 21:41
@praxis-bot-app

Copy link
Copy Markdown

Missing Signed-off-by: 47f6876, 82eab8e. All commits require sign-off (via git commit --signoff).

@praxis-bot-app

Copy link
Copy Markdown

Unsigned commits: e156a1a, 47f6876, 82eab8e. Please sign your commits.

@jordigilh
jordigilh force-pushed the jordigilh/token-rate-limit-per-app-budgets branch 2 times, most recently from d3c7813 to c644536 Compare August 20, 2026 21:59

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(token_rate_limit): per-header rate-limit bucket keys (ai#129)

Well-structured PR with thorough test coverage (112 unit tests, integration tests for all three example configs, both algorithms, both backends). The reserve/reconcile design, fail-closed Valkey behavior, and per-key budget isolation are all solid. Two issues worth addressing before merge.

Severity Count
Critical 0
Large 2
Medium 1

All comments are inline.

Comment thread filters/src/token_rate_limit/backend.rs
Comment thread docs/filters/token_rate_limit.md Outdated
Comment thread docs/filters/token_rate_limit.md
@jordigilh jordigilh changed the title feat(token_rate_limit): per-header rate-limit bucket keys (ai#129) feat(token_rate_limit): sliding-window/token-bucket rate limiting (M1/M2/M6) Aug 25, 2026

@nerdalert nerdalert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. The reservation/reconciliation split and shared backend direction are solid. I found four areas where tightening the trust boundary and hot-path behavior would make the implementation safer to operate.

Comment thread filters/src/token_rate_limit/config.rs
Comment thread filters/src/token_rate_limit/mod.rs
Comment thread filters/src/token_rate_limit/mod.rs Outdated
Comment thread filters/src/token_rate_limit/backend.rs
@asaadbalum

asaadbalum commented Aug 26, 2026

Copy link
Copy Markdown

Reviewed the full diff, correct proposal alignment (M1/M2/M6), and both algorithms working with pluggable backends.

On Brent's review comments:
Trust boundary and probe traffic: agree these are real concerns. I'd suggest tracking them as follow-up issues rather than blocking this PR since the proposal's Non-Goals already state "assumes identity resolved by upstream," so this is a known boundary.

Approving, let's get this in.

Comment thread .github/workflows/valkey.yaml
@jordigilh

Copy link
Copy Markdown
Contributor Author

Two commits landed after your approval, flagging since the diff changed:

991724a fixes Brent's Lua-safe-integer-bound comment (mod.rs:305/311) -- sliding_window was missing the same 2^53 capacity check token_bucket already had, a real gap once Valkey's Lua scripts convert it with tonumber. Added regression coverage for both algorithms.

d45e1e9: a coverage audit found every Valkey-backed test (cross-instance state, background-worker reconciliation) silently no-ops in CI -- TOKEN_RATE_LIMIT_VALKEY_URL is required but was never set anywhere, so they'd been passing by skipping, not running. Added .github/workflows/valkey.yaml (same shape as postgres.yaml) to run them for real, plus tests for the gaps that surfaced once they did.

Trust-boundary and probe-traffic are unchanged, still open per your note above and grid#101.

Let me know if you want another look given the new commits.

@nerdalert

Copy link
Copy Markdown
Member

Reviewed the full diff, correct proposal alignment (M1/M2/M6), and both algorithms working with pluggable backends.

On Brent's review comments: Trust boundary and probe traffic: agree these are real concerns. I'd suggest tracking them as follow-up issues rather than blocking this PR since the proposal's Non-Goals already state "assumes identity resolved by upstream," so this is a known boundary.

Approving, let's get this in.

@asaadbalum wfm! Added them to the post merge tracking issue. praxis-proxy/grid#101

@jordigilh the additions look good. Lets get CI passing and the review issues resolved and once @jland-redhat gives a 👍 we will get it queued for merge, ty!

@asaadbalum

Copy link
Copy Markdown

LGTM

Comment thread examples/configs/token-rate-limit-mixed-algorithms.yaml
Comment thread examples/configs/token-rate-limit-mixed-algorithms.yaml Outdated
Comment thread examples/configs/token-rate-limit-mixed-algorithms.yaml Outdated
Comment thread docs/filters/token_rate_limit.md Outdated
Comment thread examples/configs/token-rate-limit-mixed-algorithms.yaml
Comment thread examples/configs/token-rate-limit-mixed-algorithms.yaml
@alexsnaps

Copy link
Copy Markdown
Member

So, just for my understanding here, is this meant to become the TRLPolicy enforcing mechanism at some point?
It is a conscious decision that break backwards compatibility here? If I read this right probably on semantic, but certainly on "counters"? Finally, this is a 3.6 grid feature, yes? No model serving? Should we as such move this behind a feature flag? Experimental even?

@nerdalert

nerdalert commented Aug 27, 2026

Copy link
Copy Markdown
Member

So, just for my understanding here, is this meant to become the TRLPolicy enforcing mechanism at some point? It is a conscious decision that break backwards compatibility here? If I read this right probably on semantic, but certainly on "counters"? Finally, this is a 3.6 grid feature, yes? No model serving? Should we as such move this behind a feature flag? Experimental even?

Thanks for looking @alexsnaps! I think experimental makes a lot of sense! It's currently opt-in but making it explicitly experimental wfm since there is a lot of follow up work. WDYT @jordigilh? We should track something for how it ties back to TRLPolicy explicitly if there isn't already an issue.

Tagging in cc/ @jland-redhat @asaadbalum ty! Jamie already flagged the TRLPolicy CR somewhere. Ty!

@alexsnaps

Copy link
Copy Markdown
Member

Moving this forward in experimental is perfectly fine with me, to be clear. But the concerns wrt backwards compatibility are a big one to me, let's make sure we're (BU?) are all aligned on this.

If we then move forward with this implementation, we need to create follow up work on a few things (don't want to slow a PoC down tho, hence the experimental proposal, but HA, clustered Redis/sharded redis-alikes, and failure scenarios need to be addressed and probably consolidated - this currently suffer from a few pathological "edge cases").

@jland-redhat

Copy link
Copy Markdown

Moving this forward in experimental is perfectly fine with me, to be clear. But the concerns wrt backwards compatibility are a big one to me, let's make sure we're (BU?) are all aligned on this.

If we then move forward with this implementation, we need to create follow up work on a few things (don't want to slow a PoC down tho, hence the experimental proposal, but HA, clustered Redis/sharded redis-alikes, and failure scenarios need to be addressed and probably consolidated - this currently suffer from a few pathological "edge cases").

And just as a note from the MaaS and MaaSSubscription perspective at least. My hope is that we can make keep backwards with existing subscriptions that we can just translate differently on the backend. While also exposing this new functionality.

Basically an existing subscription should just be a Sliding Window config with 0 reserved tokens

@jordigilh

jordigilh commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Feature gate's in: token-rate-limit-filter Cargo feature, off by default. The PR itself is also labeled experimental, for reviewer visibility.

TRLPolicy relationship: Will open a spike shortly and link it here once filed.

HA/clustered Valkey: Will open a spike to enumerate the failure modes before committing to a design. Will link here too.

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Re-Review

Summary: Previous Large findings (TCP connection churn, phantom doc fields) have been partially addressed. The Valkey connection is now cached correctly. However, the documentation still has a critical gap: the algorithm field (required discriminator for sliding_window vs token_bucket) is completely absent from the configuration reference table.

Severity Count
Critical 0
Large 1
Medium 0

Comment thread docs/filters/token_rate_limit.md
@shaneutt
shaneutt self-requested a review as a code owner August 28, 2026 17:11
@jordigilh
jordigilh force-pushed the jordigilh/token-rate-limit-per-app-budgets branch 2 times, most recently from 04da692 to cac0838 Compare August 28, 2026 19:04
Implements the agreed M1/M2/M6 core of the token rate limiting
proposal (00121_token-rate-limiting.md in praxis-proxy/enhancements,
tracked by epic ai#121): sliding-window and token-bucket algorithms,
per-rule request/response matching, and a pluggable backend (in-memory
by default, Valkey for shared state across replicas).

Gated behind the token-rate-limit-filter Cargo feature (disabled by
default, mirroring the azure-ad/gcp-adc/http-callout pattern) so
nothing changes for consumers who don't opt in while the remaining
epic milestones (M3+) are still being designed.

Signed-off-by: Jordi Gil <jgil@redhat.com>
Covers single-rule and mixed-algorithm (sliding_window + token_bucket)
setups end to end, including the Valkey-backed path that isolates
budgets across gateway replicas. Also adds the runnable example configs
referenced from the filter's docs.

Signed-off-by: Jordi Gil <jgil@redhat.com>
The Valkey-backed tests were silently skipping in CI because nothing
set TOKEN_RATE_LIMIT_VALKEY_URL. Adds a dedicated workflow (path-scoped
so it only runs when relevant files change) plus Makefile targets that
spin up Valkey and run the gated test suite against it.

Signed-off-by: Jordi Gil <jgil@redhat.com>
generate-filter-docs dropped the discriminator field for any
#[serde(flatten)]'d struct whose type was an internally-tagged enum
(#[serde(tag = "...")]) -- e.g. rules[].algorithm on the new
token_rate_limit filter never made it into the generated table.
Synthesizes the missing row instead of silently omitting it, with a
regression test.

Signed-off-by: Jordi Gil <jgil@redhat.com>
cargo xtask generate-filter-docs output for the new filter, including
the algorithm field now that the generator handles flattened,
internally-tagged enums correctly.

Signed-off-by: Jordi Gil <jgil@redhat.com>
@jordigilh
jordigilh force-pushed the jordigilh/token-rate-limit-per-app-budgets branch from ebb9591 to 28ce5c3 Compare August 29, 2026 13:19
@jordigilh

jordigilh commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Force pushed after rebasing and removed all commits that were not part of this PR. All comments were already addressed prior to the force push, but feel free to re-review again at your discretion.

@jordigilh
jordigilh added this pull request to the merge queue Aug 29, 2026
@jordigilh
jordigilh removed this pull request from the merge queue due to a manual request Aug 29, 2026
@shaneutt
shaneutt merged commit 084d8de into praxis-proxy:main Aug 31, 2026
31 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants