Skip to content

Token rate limit reservations#190

Open
eguzki wants to merge 2 commits into
mainfrom
token-rate-limit-reservations
Open

Token rate limit reservations#190
eguzki wants to merge 2 commits into
mainfrom
token-rate-limit-reservations

Conversation

@eguzki

@eguzki eguzki commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added a proposal for token rate-limit reservations to prevent capacity races.
    • Requests can reserve estimated token capacity on arrival and commit actual usage when complete.
    • Supports configurable reservation amounts and expiry times, with safe defaults.
    • Preserves existing rejection behaviour, rate-limit headers, and all-or-nothing handling across multiple limits.
    • Includes a compatibility mode to retain current rate-limiting behaviour and a rollout safety switch.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This RFC proposes token capacity reservations on request arrival, followed by commitment of actual usage on response. It defines configuration modes, per-counter reservation state, new custom-service RPCs, storage behaviour, rollout wiring, and future mode constraints.

Changes

Token Rate-Limit Reservations

Layer / File(s) Summary
Reservation contract and request flow
rfcs/0000-token-rate-limit-reservations.md
Defines cluster and per-limit reservation settings, estimated token and TTL defaults, request-arrival reservation, response commitment, rejection semantics, and expiry handling.
Limitador reservation state and RPCs
rfcs/0000-token-rate-limit-reservations.md
Defines per-counter reservation registries, backend-specific reclamation, additive Reserve and Commit RPCs, counter updates, and storage scope.
Rollout wiring and mode constraints
rfcs/0000-token-rate-limit-reservations.md
Describes the Limitador flag, wasm-shim and operator changes, effective-state branching, unresolved questions, and possible policy-level mode overrides.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Gateway
  participant Limitador
  participant Upstream
  Gateway->>Limitador: Reserve estimated tokens
  Limitador-->>Gateway: Return admission decision
  Gateway->>Upstream: Forward admitted request
  Upstream-->>Gateway: Return actual token usage
  Gateway->>Limitador: Commit actual usage
  Limitador-->>Gateway: Remove reservation
Loading

Poem

I’m a rabbit guarding tokens tight,
Holding a handful through the night.
Reserve at dawn, commit at noon,
Let expired holds drift off like moon.
Counters hop in orderly rows—
A tidy RFC is how it grows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the RFC’s main change: introducing token rate limit reservations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch token-rate-limit-reservations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@adam-cattermole adam-cattermole moved this to In Progress in Kuadrant Jul 21, 2026
@eguzki
eguzki force-pushed the token-rate-limit-reservations branch from 025c67d to 15b0770 Compare July 21, 2026 12:19
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
@eguzki
eguzki force-pushed the token-rate-limit-reservations branch from 15b0770 to 7da814f Compare July 21, 2026 13:56
Signed-off-by: Eguzki Astiz Lezaun <eastizle@redhat.com>
@eguzki
eguzki force-pushed the token-rate-limit-reservations branch from 98b81b8 to 6f3a8b9 Compare July 23, 2026 13:48
@eguzki
eguzki marked this pull request as ready for review July 23, 2026 13:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rfcs/0000-token-rate-limit-reservations.md`:
- Line 7: Replace the placeholder Issue tracking entry in the RFC with the
actual issue URL, or remove the entry entirely if no issue exists. Ensure the
document no longer references issue `#0000`.
- Around line 86-90: Update the fenced code block containing the max_tokens CEL
expression to include a language identifier, preferably cel or text if CEL is
unsupported, while leaving the example content unchanged.
- Around line 73-77: Update the reservation examples and fallback behavior in
the reservation configuration and Reserve/Commit flow so reservation.amount is a
safe upper bound for actual usage, preventing Commit from exceeding the enforced
limit; otherwise explicitly document that enforcement is best-effort. Ensure the
example expression and fallback value do not under-reserve requests, and apply
the same contract to the additional referenced sections.
- Around line 155-169: Align CounterReservationRegistry with per-reservation
expiry by defining a keying or purge strategy that removes individual expired
ReservationEntry records rather than relying on container-level Moka or Redis
expiry. Ensure outstanding(counter) ignores and cleanup removes expired
reservations even while the counter remains active, and update the related TTL
description at the referenced section consistently.
- Around line 222-234: Update the Commit reservation flow and CommitResponse
semantics so retries with the same reservation_id are idempotent: retain durable
terminal state or otherwise distinguish already-committed IDs from
expired/unknown IDs, and do not apply actual_amount again for an
already-committed reservation. Preserve Report-style fallback only for empty,
expired, or genuinely unknown reservation IDs.
- Around line 257-285: Resolve the contradictory reservation flags in the
“Limitador-level opt-in” and “limitador” work items by specifying one flag name
and its default, then align all references to it. Define the
capability-detection or upgrade-sequencing behavior used by the reconciler so
the default CR mode does not emit Reserve/Commit actions until the target
Limitador supports and enables those RPCs, preserving Check/Report during
upgrades or when reservations are disabled.
- Line 12: Clarify the default reservation guarantee in the RFC: because
Reservation mode’s local registry does not synchronize outstanding reservations
for the distributed gossip backend or RocksDbStorage, state that the default
protects concurrency only within a single replica, or change the default so
those backends use CheckReport mode. Update the surrounding description of the
default behavior without altering rejection, headers, or multi-limit semantics.
- Around line 168-170: Define cross-counter transaction semantics for Reserve
and Commit: require a shared lock or equivalent atomic transaction across every
affected counter, perform all capacity checks before creating any reservation
entries, and roll back all changes if any check or write fails. Specify that
Commit atomically updates actual_amount and removes all live reservation
entries, or leaves every counter and reservation unchanged on failure, including
the multi-limit behavior described near lines 241-245.
- Line 96: Update the RFC’s default ttl definition to specify a deterministic
finite fallback when HTTPRoute.spec.rules[].timeouts.backendRequest is absent or
unsuitable, while retaining the route timeout when it is available. Ensure the
default behavior remains well-defined for Gateway TargetRefs spanning routes
with differing or missing timeouts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8a2d1e1-d947-4630-8979-6bac8f8b9ee9

📥 Commits

Reviewing files that changed from the base of the PR and between d883bfa and 6f3a8b9.

📒 Files selected for processing (1)
  • rfcs/0000-token-rate-limit-reservations.md

- Status: Draft
- Start Date: 2026-07-20
- RFC PR: [Kuadrant/architecture#190](https://github.com/Kuadrant/architecture/pull/190)
- Issue tracking: ~[Kuadrant/architecture#0000](https://github.com/Kuadrant/architecture/issues/0000)~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the placeholder issue-tracking link.

Line 7 points to issue #0000, which is not a usable tracking reference. Link the real issue or remove this entry before merging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` at line 7, Replace the
placeholder Issue tracking entry in the RFC with the actual issue URL, or remove
the entry entirely if no issue exists. Ensure the document no longer references
issue `#0000`.


# Summary

`TokenRateLimitPolicy` ([RFC 0013](./0013-ai-policies.md)) checks capacity when a request arrives but only reports actual token usage after the model responds, which lets concurrent in-flight requests race past a limit before any of them report usage. This RFC introduces a **reservation layer** in Limitador: on arrival, an estimated token volume is reserved and held (with a TTL) against a limit's capacity; when the response comes back, the caller commits the reservation with actual usage in a single call, releasing any over-reservation immediately. Rejection semantics, rate-limit headers, and multi-limit all-or-nothing behavior are unchanged. The feature is additive at every layer (new gRPC RPCs, new CRD field, new Limitador flag) and is enabled by default, with an explicit `CheckReport` mode to revert to today's behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files around rfcs:"
git ls-files | rg '^rfcs/0000-token-rate-limit-reservations\.md$|^rfcs/|limitador|reservation|reserv' || true

echo
echo "File slice:"
if [ -f rfcs/0000-token-rate-limit-reservations.md ]; then
  wc -l rfcs/0000-token-rate-limit-reservations.md
  sed -n '1,320p' rfcs/0000-token-rate-limit-reservations.md | cat -n
fi

Repository: Kuadrant/architecture

Length of output: 29535


Clarify the default reservation guarantee.

The default mode is now advertised as automatically closing the concurrency race, but Reservation mode uses a local registry for the distributed gossip backend and for RocksDbStorage. Replicas can therefore still admit against incomplete outstanding state. Either disable default Reservation mode for those backends or reword the default guarantee to reflect single-replica protection only.

🧰 Tools
🪛 LanguageTool

[grammar] ~12-~12: Did you mean “responses”?
Context: ...orts actual token usage after the model responds, which lets concurrent in-flight reques...

(PREPOSITION_VERB)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` at line 12, Clarify the default
reservation guarantee in the RFC: because Reservation mode’s local registry does
not synchronize outstanding reservations for the distributed gossip backend or
RocksDbStorage, state that the default protects concurrency only within a single
replica, or change the default so those backends use CheckReport mode. Update
the surrounding description of the default behavior without altering rejection,
headers, or multi-limit semantics.

Comment on lines +73 to +77
reservation:
# estimated tokens to hold before the model responds
amount: "uint(size(requestBodyJSON('/messages')) / 4)"
# how long to hold it before automatically releasing it
ttl: "duration('30s')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Require reservation amounts to be safe upper bounds.

Reserve admits on the estimated amount, while Commit applies actual_amount unconditionally. The example expression and the fallback value of 100 can under-reserve real usage, so concurrent requests can still push a counter beyond its limit. Require reservation.amount to be an upper bound, or explicitly document that enforcement is best-effort.

Also applies to: 80-94, 241-245

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` around lines 73 - 77, Update the
reservation examples and fallback behavior in the reservation configuration and
Reserve/Commit flow so reservation.amount is a safe upper bound for actual
usage, preventing Commit from exceeding the enforced limit; otherwise explicitly
document that enforcement is best-effort. Ensure the example expression and
fallback value do not under-reserve requests, and apply the same contract to the
additional referenced sections.

Comment on lines +86 to +90
```
has(requestBodyJSON('/max_tokens'))
? uint(requestBodyJSON('/max_tokens'))
: 100
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced CEL example.

The unlabelled fence triggers Markdownlint MD040. Use an appropriate identifier such as cel, or text if the renderer does not support CEL.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 86-86: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` around lines 86 - 90, Update the
fenced code block containing the max_tokens CEL expression to include a language
identifier, preferably cel or text if CEL is unsupported, while leaving the
example content unchanged.

Source: Linters/SAST tools


Note that the `amount` default assumes `requestBodyJSON('/max_tokens')` is reliably readable at this point, which is itself unresolved (see [Unresolved questions](#unresolved-questions)). If it isn't, this default degrades to the flat fallback constant for every request.

- **Default `ttl`**: the route's own configured backend-request timeout (Gateway API `HTTPRoute.spec.rules[].timeouts.backendRequest`). A reservation only needs to outlive a request for as long as that request could legitimately still be in flight, and the gateway already has an opinion about that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate RFC and related lines =="
fd -a '0000-token-rate-limit-reservations\.md$' . || true
file=$(fd '0000-token-rate-limit-reservations\.md$' . | head -n1 || true)
if [ -n "${file:-}" ]; then
  echo "FILE=$file"
  wc -l "$file"
  sed -n '1,160p' "$file" | nl -ba
fi

echo
echo "== search for ttl/default/fallback/backendRequest in RFC =="
if [ -n "${file:-}" ]; then
  rg -n -i 'ttl|fallback|backendrequest|backend-request|timeout|duration|reserve|reservation' "$file" || true
fi

echo
echo "== search backendRequest mentions in repo =="
rg -n -i 'backendRequest|backend-request|HTTPRoute.*timeout|timeouts' . || true

Repository: Kuadrant/architecture

Length of output: 396


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="./rfcs/0000-token-rate-limit-reservations.md"

echo "== RFC excerpt =="
python3 - <<'PY' "$file"
import sys
for line in open(sys.argv[1], encoding="utf-8", newline="\n"):
    print(line.rstrip())
PY

echo
echo "== TTL/fallback/timeout/search terms =="
grep -n -i 'ttl\|fallback\|backendrequest\|backend-request\|timeout\|duration\|reserve\|reservation' "$file" || true

Repository: Kuadrant/architecture

Length of output: 42461


🌐 Web query:

Gateway API HTTPRoute spec rules timeouts backendRequest optional timeout documentation

💡 Result:

In the Kubernetes Gateway API, the backendRequest timeout is an optional field within the HTTPRoute rule specification [1][2]. Key details regarding the backendRequest timeout include: Purpose: It defines a timeout for an individual request sent from the Gateway to a backend service. It covers the duration from when the request starts being sent from the gateway until the full response is received from the backend [1][3]. Optionality: The timeouts field (including backendRequest) is optional. If left unspecified, the timeout behavior is implementation-specific [1][4]. Relationship to Request Timeout: Because the overall request timeout encompasses the backendRequest timeout, the value of backendRequest must not exceed the value of the request timeout [1][2]. Value Format: Timeouts are defined as Gateway API Duration strings [2]. A zero-valued timeout (e.g., "0s") must be interpreted as disabling the timeout [1][4]. Any valid non-zero timeout must be at least 1 millisecond [1][3]. Support Level: The configuration of backendRequest (HTTPRouteBackendTimeout) is considered an Extended support feature [2]. When configuring an HTTPRoute, you include the timeout under the rules section as follows: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: timeout-example spec: rules: - timeouts: request: 10s backendRequest: 2s backendRefs: - name: timeout-svc port: 8080

Citations:


Define a deterministic TTL fallback.

HTTPRoute.spec.rules[].timeouts.backendRequest is optional and implementation-specific when omitted, and a TargetRef on Gateway can span routes with different or absent route timeouts. This RFC still says reservations are enabled by default, so it should define a finite fallback value rather than relying on an optional Gateway API field that may be missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` at line 96, Update the RFC’s
default ttl definition to specify a deterministic finite fallback when
HTTPRoute.spec.rules[].timeouts.backendRequest is absent or unsuitable, while
retaining the route timeout when it is available. Ensure the default behavior
remains well-defined for Gateway TargetRefs spanning routes with differing or
missing timeouts.

Comment on lines +155 to +169
CounterReservationRegistry: CounterKey -> [ReservationEntry]

// A single outstanding hold against one counter.
ReservationEntry {
reservation_id: ReservationId // opaque; the SAME id is stored under every
// counter this reservation touched, so one
// token released by the caller settles all
// of them in a single Commit call
amount: u64 // estimated tokens reserved, uniform across
// every counter this reservation touched
expires_at: Timestamp // see "TTL and expiry" below
}

// Capacity available for a counter must now account for outstanding holds:
outstanding(counter) = sum(e.amount for e in registry[counter] if e.expires_at > now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Make per-reservation expiry compatible with the registry shape.

The registry is described as CounterKey -> [ReservationEntry], but Moka entry expiry and Redis key-level EXPIRE expire the containing cache entry/hash, not individual reservations. Define per-entry keying or an explicit purge strategy; otherwise expired fields can accumulate whenever a counter remains active.

Also applies to: 191-192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` around lines 155 - 169, Align
CounterReservationRegistry with per-reservation expiry by defining a keying or
purge strategy that removes individual expired ReservationEntry records rather
than relying on container-level Moka or Redis expiry. Ensure
outstanding(counter) ignores and cleanup removes expired reservations even while
the counter remains active, and update the related TTL description at the
referenced section consistently.

Comment on lines +168 to +170
// Capacity available for a counter must now account for outstanding holds:
outstanding(counter) = sum(e.amount for e in registry[counter] if e.expires_at > now)
admit if counter.value + outstanding(counter) + requested_amount <= limit

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '^rfcs/0000-token-rate-limit-reservations\.md$|0000-token-rate-limit-reservations' || true

if [ -f rfcs/0000-token-rate-limit-reservations.md ]; then
  echo "--- Relevant sections ---"
  sed -n '140,260p' rfcs/0000-token-rate-limit-reservations.md
fi

echo "--- Atomicity keywords ---"
rg -n "atomic|transaction|rollback|lock|partial|all-or-nothing|all or nothing|Reserve|Commit|multi-limit" rfcs/0000-token-rate-limit-reservations.md || true

Repository: Kuadrant/architecture

Length of output: 16979


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- Outline/keywords for Reserve/Commit implementation mentions ---"
rg -n "Reserve|Commit|all-or-nothing|all or nothing|atomic|transaction|rollback|lock|multi-limit|outstanding|reservation_id|update_counter|CounterReservationRegistry" . --glob '!target/**' --glob '!build/**' --glob '!dist/**' | head -n 240 || true

Repository: Kuadrant/architecture

Length of output: 30921


Define cross-counter atomicity for reservations.

Reserve stores CounterReservationRegistry[counter] per counter and the RFC says multi-limit behaviour is unchanged, but it does not specify the transaction/locking/rollback semantics needed when a request touches several counters. Define how Reserve keeps the all-or-nothing check-all-then-create-all invariant, and how Commit either updates actual_amount and removes the live reservation entries atomically or fails/rolls back without leaving partial holds or charges; this applies to lines 241-245 as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` around lines 168 - 170, Define
cross-counter transaction semantics for Reserve and Commit: require a shared
lock or equivalent atomic transaction across every affected counter, perform all
capacity checks before creating any reservation entries, and roll back all
changes if any check or write fails. Specify that Commit atomically updates
actual_amount and removes all live reservation entries, or leaves every counter
and reservation unchanged on failure, including the multi-limit behavior
described near lines 241-245.

Comment on lines +222 to +234
message CommitRequest {
string domain = 1;
repeated RateLimitDescriptor descriptors = 2; // used to re-resolve counters -- see below
string reservation_id = 3; // optional; empty behaves like plain Report
uint64 actual_amount = 4;
}

message CommitResponse {
bool reservation_released = 1; // diagnostic only: false if reservation_id
// was empty, unknown, or already expired,
// and actual_amount was applied directly
// instead (Report-style) -- see below
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make Commit idempotent for a reservation ID.

After a successful commit, a retry with the same ID finds no live reservation and falls back to applying actual_amount again. A lost response can therefore double-count usage. Add durable deduplication/terminal state, or distinguish an already-committed ID from an expired or unknown ID.

Also applies to: 241-245

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` around lines 222 - 234, Update
the Commit reservation flow and CommitResponse semantics so retries with the
same reservation_id are idempotent: retain durable terminal state or otherwise
distinguish already-committed IDs from expired/unknown IDs, and do not apply
actual_amount again for an already-committed reservation. Preserve Report-style
fallback only for empty, expired, or genuinely unknown reservation IDs.

Comment on lines +257 to +285
### Limitador-level opt-in

A server-level flag, `--enable-reservations` (default: `false`, i.e. reservations disabled), independent of anything the Kuadrant CR requests. When not set, `Reserve`/`Commit` are not registered (or return `UNIMPLEMENTED`). This exists as defense-in-depth for operators who want a hard kill-switch regardless of what any CR asks for.

## Expected work by component

**limitador**

- `ReservationEntry` type and per-counter registry: persisted (dedicated Moka cache for `InMemoryStorage`, hash-field-backed for `RedisStorage`/`AsyncRedisStorage`/`CachedRedisStorage`) or local-memory-only (`RocksDbStorage`, distributed backend), per [Storage backend scope for this RFC](#storage-backend-scope-for-this-rfc).
- `RateLimiter::reserve` / `RateLimiter::commit_reservation` (and async twins), composed from existing `counters_that_apply` / `update_counter` / admission-check logic.
- `Reserve` / `Commit` proto messages and RPCs on the existing Kuadrant service, implemented alongside (not replacing) `CheckRateLimit`/`Report`.
- `--disable-reservations` server flag.
- Concurrency/race tests exercising the scenario in [Motivation](#motivation) directly.

**wasm-shim**

- TBD. How the wasm shim actually issues the request-phase `Reserve` call and the response/stream-end-phase `Commit` call.
- `ServiceType` gains `RateLimitReserve` / `RateLimitCommit`, additive to the existing open enum (`RateLimit | RateLimitCheck | RateLimitReport | Auth | Tracing | Dynamic`). Existing variants and their handling in `DynamicService`/`DynamicTask` are untouched.
- `reservation.amount` / `reservation.ttl` are evaluated exactly like `hits_addend` is today: via `message_builder` CEL on the `Reserve` action.
- The `reservation_id` returned by `Reserve` is captured via an `on_reply` → `Store` action into `ReqRespCtx.stored_values`, the same mechanism already used to carry state between phases of one HTTP transaction . It needs no host export, since it's read back within the same `KuadrantFilter` instance later in the same request.

**kuadrant-operator**

- `Kuadrant` CR field (`spec.tokenRateLimiting.mode`, enum `Reservation`/`CheckReport`) and `TokenRateLimitPolicy` `reservation` field (`amount`, `ttl` CEL expressions), including defaults/overrides merge behavior.
- Default `amount`/`ttl` CEL generation for limits that omit `reservation`
- Reconciler branch generating `Reserve`/`Commit` vs `Check`/`Report` wasm actions based on effective state.
- **Effectively active**: generate a `Reserve` action (new `ServiceType::RateLimitReserve`) at request phase, with a `message_builder` evaluating `reservation.amount`/`reservation.ttl`, and a `Commit` action (new `ServiceType::RateLimitCommit`) at response/stream-end phase.
- **Effectively inactive**: generate exactly what is generated today, i.e. `ratelimit-check-service` (`hits_addend=0`) + `ratelimit-report-service` (`hits_addend=<actual>`).
- Docs and e2e tests covering both modes and the upgrade-window transition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Resolve the feature-flag and upgrade-window contract.

Line 259 defines --enable-reservations with a default of false, while Line 268 requires --disable-reservations; meanwhile the default CR mode emits Reserve/Commit. Specify one flag and default, and define capability detection or upgrade sequencing so the operator never emits new RPCs to an older or disabled Limitador.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~259-~259: Possible missing comma found.
Context: ...epth for operators who want a hard kill-switch regardless of what any CR asks for. ##...

(AI_HYDRA_LEO_MISSING_COMMA)


[grammar] ~266-~266: The usual collocation for “composed” is “of”, not “from”. Did you mean “composed of”?
Context: ...:commit_reservation(and async twins), composed from existingcounters_that_apply/updat...

(COMPOSE_FROM_OF)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rfcs/0000-token-rate-limit-reservations.md` around lines 257 - 285, Resolve
the contradictory reservation flags in the “Limitador-level opt-in” and
“limitador” work items by specifying one flag name and its default, then align
all references to it. Define the capability-detection or upgrade-sequencing
behavior used by the reconciler so the default CR mode does not emit
Reserve/Commit actions until the target Limitador supports and enables those
RPCs, preserving Check/Report during upgrades or when reservations are disabled.


### Limitador-level opt-in

A server-level flag, `--enable-reservations` (default: `false`, i.e. reservations disabled), independent of anything the Kuadrant CR requests. When not set, `Reserve`/`Commit` are not registered (or return `UNIMPLEMENTED`). This exists as defense-in-depth for operators who want a hard kill-switch regardless of what any CR asks for.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Kuadrant default is to enable, and the Limitador default is to disable?
--enable-reservations here vs --disable-reservations a few lines below vs disable: false in kuadrant CR .
Even if the CR will have an enum of: strategy: reservation/default (or something like this, as discussed at the community call) we might want the same "default" state across all components

expires_at(counter) = min(
now + reservation.ttl, // caller-supplied TTL (CEL-computed, see above)
current_window_expiry(counter), // this counter's own window boundary
server_max_reservation_ttl // operator-configured safety clamp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How do we set the server_max_reservation_ttl?

}

// Capacity available for a counter must now account for outstanding holds:
outstanding(counter) = sum(e.amount for e in registry[counter] if e.expires_at > now)

@maksymvavilov maksymvavilov Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This might be nothing, but this will loop over every reservation/request in process (O(n) for n reservations).
Is keeping a single counter that each reservation will update a better idea (O(1))?


`TokenRateLimitPolicy` ([RFC 0013](./0013-ai-policies.md)) checks capacity when a request arrives but only reports actual token usage after the model responds, which lets concurrent in-flight requests race past a limit before any of them report usage. This RFC introduces a **reservation layer** in Limitador: on arrival, an estimated token volume is reserved and held (with a TTL) against a limit's capacity; when the response comes back, the caller commits the reservation with actual usage in a single call, releasing any over-reservation immediately. Rejection semantics, rate-limit headers, and multi-limit all-or-nothing behavior are unchanged. The feature is additive at every layer (new gRPC RPCs, new CRD field, new Limitador flag) and is enabled by default with an explicit opt-out.

# Motivation

@maksymvavilov maksymvavilov Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was looking at it from the Egress perspective: adding the Egress gateway as motivation will make the case for this much stronger (not that it needs that, but hey, why not?)

Also, for the egress - we will be making a heavy guess on the reservation size. Maybe the default of 100 is too generous? I'd rather over-reserve with ~5k or something like that. What do you think (make it configurable in kuadrant CR?)?
I'm talking here about the default that is hardcoded here

kind: Kuadrant
spec:
tokenRateLimiting:
mode: Reservation # default; set to CheckReport to revert every

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.

Yes ok this feels better than a kill switch. I don't know what reason someone would chose the old behaviour though, but if it may be the case we add new "modes" then a choice is useful.


### Limitador-level opt-in

A server-level flag, `--enable-reservations` (default: `false`, i.e. reservations disabled), independent of anything the Kuadrant CR requests. When not set, `Reserve`/`Commit` are not registered (or return `UNIMPLEMENTED`). This exists as defense-in-depth for operators who want a hard kill-switch regardless of what any CR asks for.

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.

interesting so I wonder is this enough and so can avoid the additional API in the kuadrant CR? This boils down to the same type of operation. Patch a resource with a new value?

@maleck13 maleck13 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.

This looks good from my perspective. I only question the need for the Kuadrant level api to set the mode. If we have a "bail out option" on limitador do we really need another one on the kuadarant CR? Its not completely clear to me when a user would actually want the old behaviour to come back beyond some catostrophic bug?

That said I don't think this is a blocker it just sets a precendent where more and more policy level behaviour could be added to the Kuadrant CR.

Generally approve of the changes outlined though

@jland-redhat

Copy link
Copy Markdown

This is a cool idea!

So one question that I have that is probably out of scope here but I want to bring up the topic.

The idea of rate limiting in a "batch" setting has been brought up many times. It would be great if there was a way to deny request BEFORE they were put on the queue but that becomes very difficult...

So question here is do we think could eventually expand this "reservation" system to help take care of that problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

5 participants