You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Budget gauge writes are not gated when budgets are disabled #940
On deployments where budget enforcement is disabled (BudgetClient::disabled(), i.e. standalone mode with no cp-api), every request still pays a constant gauge write: chat.rs calls record_budget_gauges(..., None) -> clear_budget_gauges -> aisix_budget_details_present{api_key_id,team_id,user_id} = 0. The write costs ~0.2us/request (label-key construction dominates; the cached-handle path still builds the 3-label key every time).
Why it was not included in the zero-config fast-path package
Gating the write on "budgets disabled" removes the aisix_budget_details_present series for keys that have traffic, which violates the /metrics series-compatibility invariant the package is shipped under. The alternatives all have worse trade-offs:
Value-memoisation only saves the atomic store, not the label-key construction (the dominant cost).
A periodic resync over snapshot.apikeys would inflate the series set from "keys with traffic" to "all configured keys" — unacceptable on deployments with very large key counts.
What this issue asks for
Decide and implement one of:
Accept the series change: when budgets are disabled, do not emit aisix_budget_details_present at all (documented behavior change, changelog entry). The gauge is arguably meaningless when no budget subsystem is active.
Emit the 0-value series once per key (first request) instead of every request, preserving series presence with a bounded write cost.
Keep the status quo and close as won't-fix with rationale.
Option 2 preserves the observable contract exactly (a gauge is level-triggered; re-writing 0 every request is redundant) and is likely the cheapest safe form.
Size
~0.2us/request on the zero-config hot path (~4% of the package's total recoverable time). Low priority, pure telemetry-cost hygiene.
#925 - perf(metrics): per-worker handle cache for every per-request emit path [merged]
📝 Issue Planner
Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.
Create Plan
🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your .coderabbit.yaml:
issue_enrichment:
auto_enrich:
enabled: false
💬 Have feedback or questions? Drop into our discord!
Closing as not planned. The design question does have a fourth candidate, and it costs more structure than the 0.2µs is worth.
Option 2 cannot skip the work it exists to skip
The per-request cost of clear_budget_gauges is: build the lookup key into the worker's reusable buffer (instance prefix + site + the three label values), FNV-hash it, probe the thread-local handle map, then gauge.set(0.0).
To answer "has this label set already been published?" you have to identify the label set — which is the key build and the hash, i.e. everything except the final atomic store. Memoising the value saves only that store, which the issue already identifies as the small half.
Making the memo cheaper means keying it on something other than the label values
Each candidate breaks something:
A "published" flag on the Arc<ResourceEntry<ApiKey>>. Genuinely cheap — one relaxed load — and it keeps the series set exactly "keys with traffic". But a series is registered in a recorder, and Metrics deliberately supports more than one instance in a process; worker_key_prefix exists for precisely that reason. An entry-level flag is recorder-agnostic, so the first instance publishes and the second never does. Making it correct means keying by (entry, recorder), which is the keyed lookup we were trying to avoid.
Caching the metrics::Gauge handle on the entry. Same problem, and additionally puts a recorder handle inside a config-snapshot type.
Option 1 is the only form that removes the work outright, and it spends an invariant
Not emitting aisix_budget_details_present when budgets are disabled changes /metrics output. The zero-config fast-path package (#944) shipped under a byte-identical series-set invariant, verified against a 209-series diff. Spending that invariant here is not a trade worth making.
Size
~0.2µs against the current ~102.4µs/request baseline is 0.2% — roughly 7× below the resolution of a single same-rig A/B leg (±1.5µs), so it would not be measurable even if it were free. Same call, and the same reasoning, as #924.
Left as is. Reopen if the series-compatibility invariant is ever relaxed deliberately, at which point option 1 becomes a two-line change.
Activity
coderabbitai commentedon Aug 12, 2026
🔗 Related PRs
#925 - perf(metrics): per-worker handle cache for every per-request emit path [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
membphis commentedon Aug 12, 2026
Closing as not planned. The design question does have a fourth candidate, and it costs more structure than the 0.2µs is worth.
Option 2 cannot skip the work it exists to skip
The per-request cost of
clear_budget_gaugesis: build the lookup key into the worker's reusable buffer (instance prefix + site + the three label values), FNV-hash it, probe the thread-local handle map, thengauge.set(0.0).To answer "has this label set already been published?" you have to identify the label set — which is the key build and the hash, i.e. everything except the final atomic store. Memoising the value saves only that store, which the issue already identifies as the small half.
Making the memo cheaper means keying it on something other than the label values
Each candidate breaks something:
Arc<ResourceEntry<ApiKey>>. Genuinely cheap — one relaxed load — and it keeps the series set exactly "keys with traffic". But a series is registered in a recorder, andMetricsdeliberately supports more than one instance in a process;worker_key_prefixexists for precisely that reason. An entry-level flag is recorder-agnostic, so the first instance publishes and the second never does. Making it correct means keying by (entry, recorder), which is the keyed lookup we were trying to avoid.metrics::Gaugehandle on the entry. Same problem, and additionally puts a recorder handle inside a config-snapshot type.Option 1 is the only form that removes the work outright, and it spends an invariant
Not emitting
aisix_budget_details_presentwhen budgets are disabled changes/metricsoutput. The zero-config fast-path package (#944) shipped under a byte-identical series-set invariant, verified against a 209-series diff. Spending that invariant here is not a trade worth making.Size
~0.2µs against the current ~102.4µs/request baseline is 0.2% — roughly 7× below the resolution of a single same-rig A/B leg (±1.5µs), so it would not be measurable even if it were free. Same call, and the same reasoning, as #924.
Left as is. Reopen if the series-compatibility invariant is ever relaxed deliberately, at which point option 1 becomes a two-line change.