Skip to content

Rate limit and quota nodes

Both of these are gate nodes: they sit in front of expensive work and split the run into allow and deny. They are compute (activity) nodes. The difference is what they count:

ratelimit quota
Counts requests — one hit each time the node runs tokens — model usage already recorded on the run's spans
Trips when the hit count in the window passes limit recorded usage reaches budgetTokens
Backed by a fixed-window counter in Postgres a read of the agent's usage spans (never re-metered)
Typical use "no more than 30 calls a minute per user" "no more than 100k tokens an hour for this agent"

Both keep the input value on allow and emit a clean deny result on deny — they never hang or error.

Shared ports

Port Direction Required Description
in in yes The value passing through the gate.
allow out — Carries the input through unchanged when the gate lets it pass.
deny out — Carries { "denied": true, "reason": "…" } when the gate trips.

Exactly one of allow / deny activates per run; the other branch is dead and everything downstream of it is skipped.

The key: keyExpr

Only ratelimit counts per key. Its request counter is bucketed by keyExpr, so different callers get separate request budgets. keyExpr decides the key:

  • An input reference such as $in.in.userId resolves a per-caller key from the input — each distinct value gets its own bucket.
  • Any other string is used as a literal — a single shared bucket for everyone. (An $in reference that can't be resolved falls back to the literal string, so a bad reference never crashes the run.)

For unattended runs, the natural key comes from the trigger that fired the agent — resolve it from the input the trigger delivers.

quota ignores keyExpr today. The token budget is summed per agent, not per key — every caller draws from the same budget no matter what keyExpr is set to. The field is still required by the schema, but it has no effect on a quota gate (per-user token attribution is not yet available).

ratelimit

Denies once too many requests hit the same key inside a fixed time window.

Configuration

Field Type Default Description
keyExpr string (required) "" The per-caller key (see above).
limit integer (required) 0 Requests allowed per key per window. Set a real positive value.
windowSeconds integer (required) 0 The window length in seconds. Set a real positive value.
policy deny | wait deny Leave at deny, which emits the deny port. wait (a bounded suspend) is named in the schema but not yet implemented — use deny.

What counts and when it trips

Each time a run reaches the node, it records one hit against <agent>:<node>:<key> for the current window and then denies once the count is greater than limit — so limit requests pass and the next one denies. The deny reason reads rate limit exceeded (<limit>/<windowSeconds>s).

Fixed windows, not sliding

The window is floor(now / windowSeconds); the count resets to zero at each boundary. This is a simple fixed window — a burst straddling a boundary can briefly exceed the nominal rate. It is a lightweight throttle, not a precise sliding-window limiter.

quota

Denies once the agent has already spent its token budget in the window. It reads usage that model calls recorded; it does not re-measure anything.

Configuration

Field Type Default Description
keyExpr string (required) "" Required by the schema but inert for quota — the budget is summed per agent, not per key (see above).
budgetTokens integer (required) 0 Token budget per window. Set a real positive value.
windowSeconds integer (required) 0 The window length in seconds. Set a real positive value.
source constant "spans" Fixed. Usage is read from existing spans; there is no other source.

What counts and when it trips

The gate sums gen_ai.usage.total_tokens across the agent's model calls within the last windowSeconds and denies once that total is at or above budgetTokens. The deny reason reads token budget exceeded (<budgetTokens>/<windowSeconds>s).

  • Every model call in the agent contributes — including a model guardrail's judge call.
  • Scope is per agent today: the total is summed across all of the agent's model calls, regardless of keyExpr (per-user attribution is not yet available).
  • If no usage was recorded — for example an upstream that reports none — the sum is 0 and the gate allows.

Shared behavior notes

  • Deny is a clean result, not an error. A tripped gate keeps the run completed and sends { "denied": true, "reason": "…" } out deny. The run never hangs and never returns a 500.
  • Wire the deny port. If you leave deny unwired, the deny branch reaches no output and the run finishes with an honest empty-output note. Route deny to its own output (a "try again later" message) so a throttled caller gets a clear answer.
  • allow passes the input straight through to the protected pipeline.
  • Gates are soft policy. On a durable resume, a re-run step can re-count a rate-limit hit, and quota reads whatever usage has been journaled so far. Treat these as guardrails against runaway cost, not exact financial controls.

Worked example

Throttle a public agent to 30 requests per user per minute (keyed per caller), then cap the whole agent at 50k tokens per hour, before the model runs.

graph LR
  IN[input] --> RL{ratelimit}
  RL -->|allow| Q{quota}
  RL -->|deny| BUSY[output: slow down]
  Q -->|allow| LLM[llm] --> OUT[output: answer]
  Q -->|deny| CAP[output: budget reached]

The two gate configs:

{ "keyExpr": "$in.in.userId", "limit": 30, "windowSeconds": 60, "policy": "deny" }
{ "keyExpr": "$in.in.userId", "budgetTokens": 50000, "windowSeconds": 3600, "source": "spans" }

Each deny branch ends at its own output, so a throttled or over-budget caller gets a specific message and the model is never reached.

Works well with

  • Guardrail node — the content check that pairs with these spend checks.
  • Triggers — where the per-caller key comes from on unattended runs.
  • Observability — the token usage quota reads is recorded here.
  • Input references — the $in grammar keyExpr uses.