See also (wiki): litellm-budget-governance · ai-cost-chargeback-showback
Pillar: 19 — Agent Frameworks
Date: 2026-06-17
Audience: Fortune 500 platform engineers running LiteLLM as an AI gateway
Scope: Budget controls, rate limits, alerting, IaC, cost estimation, and cost-based routing
1.1 Core Parameters
| Parameter | Type | Description |
|---|---|---|
max_budget |
float (USD) | Hard spending cap for the key |
budget_duration |
string | Window before spend resets |
soft_budget |
float (USD) | Alert threshold (does not block) |
model_max_budget |
dict | Per-model budget caps within a single key |
temp_budget_increase |
float | Temporary additive increase to max_budget |
temp_budget_expiry |
string | Expiry of the temporary increase |
Valid budget_duration strings: "30s", "30m", "30h", "1d", "7d", "10d", "30d", "90d", "1mo", "2mo". The format is <integer><unit> where unit is s/m/h/d/mo.
1.2 Key Generation with Budget
curl -X POST 'http://localhost:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"key_alias": "team-alpha-prod",
"max_budget": 500.0,
"budget_duration": "30d",
"soft_budget": 400.0,
"tpm_limit": 100000,
"rpm_limit": 600,
"model_max_budget": {
"gpt-4o": {"max_budget": 200.0, "budget_duration": "30d"},
"claude-3-5-sonnet": {"max_budget": 150.0, "budget_duration": "30d"}
}
}'
1.3 Multiple Concurrent Budget Windows
A single key can carry multiple budget windows that reset independently. This prevents daily runaway from exhausting the monthly cap:
curl -X POST 'http://0.0.0.0:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"budget_limits": [
{"budget_duration": "24h", "max_budget": 25},
{"budget_duration": "30d", "max_budget": 500}
]
}'
Note: Multiple concurrent windows were added in 2026. See Issue #28235 for tracking status.
1.4 Temporary Budget Override (Emergency)
curl -X POST 'http://localhost:4000/key/update' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{"key": "sk-abc123", "temp_budget_increase": 100, "temp_budget_expiry": "48h"}'
1.5 Soft vs Hard Limits
| Behavior | Mechanism |
|---|---|
Hard limit (max_budget) |
Blocks requests once exceeded. Returns BudgetExceededError / HTTP 429. Spend tracked in LiteLLM_VerificationTokenTable. |
Soft limit (soft_budget) |
Sends email alert when crossed; requests continue. Requires email integration (SendGrid, Resend, or SMTP). |
There is no native “warn at 80%” threshold on virtual keys. Approximate workaround: set soft_budget = max_budget * 0.8 and max_budget separately. For team-level alerts, the soft budget is configured on the team object (see §2.3).
1.6 Budget Reset Behavior
- Budget resets are handled by a background cron job that runs by default every 10 minutes (
proxy_budget_rescheduler_min_timeingeneral_settings). - If
budget_durationis not set, the budget never resets. - When a key’s budget window expires, spend resets to zero and the window restarts from that moment.
- Manual reset is not exposed as a dedicated endpoint; use
/key/updatewithspend=0via the admin API or direct DB manipulation.
Known bug: Issue #25495 — Organization-level budget_duration does not reset after expiry (spend field never zeroed). Affects orgs, not virtual keys directly. Verify behavior for any org-scoped key before production rollout.
Known bug: Issue #27735 — BudgetExceededError fires against stale Redis reservation values; /key/info shows spend below max while requests are being blocked. Root cause: async spend reservation not synchronized with DB truth. Status: open as of June 2026. Mitigation: set max_budget 10–15% above your true intended limit if false-block rate is unacceptable.
2. Team Budget Controls
2.1 Budget Hierarchy
Organization (optional)
└─ Team (max_budget, budget_duration, soft_budget)
├─ Team Member (max_budget_in_team — ceiling within team pool)
└─ Virtual Key (max_budget — independent per-key cap)
Precedence rule: “If a key belongs to a team, the team budget is applied, not the user’s personal budget.” Key-level max_budget acts as an additional ceiling; the binding limit is whichever is hit first.
2.2 Creating a Team with Budget
curl -X POST 'http://0.0.0.0:4000/team/new' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"team_alias": "data-science",
"max_budget": 2000.0,
"budget_duration": "30d",
"tpm_limit": 5000000,
"rpm_limit": 3000
}'
The response returns a team_id UUID. All keys generated with "team_id": "<uuid>" share the team’s budget pool.
2.3 Team Soft Budget Alerts
curl -X POST 'http://localhost:4000/team/update' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"team_id": "de35b29e-6ca8-4f47-b804-2b79d07aa99a",
"soft_budget": 1600.0,
"metadata": {
"soft_budget_alerting_emails": ["platform-lead@corp.com", "finops@corp.com"]
}
}'
Alert deduplication window: 24 hours (one email per team per day, regardless of how many requests cross the threshold). No proxy restart required when adding soft budgets.
2.4 Per-Member Spending Cap Within a Team
# 1. Create or update user
curl -X POST 'http://localhost:4000/user/new' \
-d '{"user_id": "alice@corp.com"}'
# 2. Add to team with individual ceiling
curl -X POST 'http://localhost:4000/team/member_add' \
-d '{
"team_id": "de35b29e-...",
"member": {"role": "user", "user_id": "alice@corp.com"},
"max_budget_in_team": 200.0
}'
2.5 Default Budget for JWT Auto-Provisioned Teams
# config.yaml
litellm_jwtauth:
team_id_upsert: true
team_id_jwt_field: "team_id"
litellm_settings:
default_team_settings:
- team_id: "default-settings"
max_budget: 500.0
budget_duration: "30d"
3. Per-User (End-User) Budget Controls
For pass-through end users (consumers of an app that calls LiteLLM), budgets can be enforced via the user parameter in completions without creating full user accounts:
# config.yaml
litellm_settings:
max_end_user_budget: 0.05 # $0.05 per user per budget_duration
# Application code — pass end-user ID
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
user="end-user-tenant-id-xyz" # tracked against max_end_user_budget
)
For internal users needing full spend visibility, use POST /user/new with max_budget + budget_duration.
4. Model-Level Rate Limits (TPM/RPM)
4.1 Per-Key Rate Limits
curl -X POST 'http://localhost:4000/key/generate' \
-d '{
"tpm_limit": 50000,
"rpm_limit": 300,
"max_parallel_requests": 20
}'
By default, TPM counts total tokens (input + output). Override in config.yaml:
litellm_settings:
token_rate_limit_type: "input" # "input" | "output" | "total" (default)
4.2 Per-Model TPM/RPM Within a Key
curl -X POST 'http://localhost:4000/key/generate' \
-d '{
"metadata": {
"model_rpm_limit": {"gpt-4o": 100, "claude-3-5-sonnet": 200},
"model_tpm_limit": {"gpt-4o": 50000, "claude-3-5-sonnet": 80000}
}
}'
4.3 Rate Limit Tiers (Enterprise)
The /budget/new + budget_id system (Enterprise) lets you define reusable rate-limit templates and assign them to keys without re-specifying limits per key:
# Create tier
curl -X POST 'http://localhost:4000/budget/new' \
-d '{"budget_id": "standard-tier", "rpm_limit": 300, "tpm_limit": 100000, "max_budget": 500}'
# Assign to key
curl -X POST 'http://localhost:4000/key/generate' \
-d '{"budget_id": "standard-tier"}'
# Upgrade key to premium tier — no key rotation required
curl -X POST 'http://localhost:4000/key/update' \
-d '{"key": "sk-abc123", "budget_id": "premium-tier"}'
4.4 LiteLLM vs. Provider-Side Enforcement (Double Enforcement)
LiteLLM enforces TPM/RPM limits before the request reaches the provider. Provider limits (e.g., AWS Bedrock service quotas, OpenAI account-level TPM) fire after. Both layers are independent. At scale:
- LiteLLM’s enforcement is in-process (Redis-backed for multi-node) — fast but subject to the stale-reservation bug documented in Issue #28146.
- Provider limits return
RateLimitError; LiteLLM’s router fallback logic handles these if configured — but see §4.5.
Known bug: Issue #28146 — LiteLLM v1.85.0: virtual keys with TPM/RPM limits inject internal _litellm_* parameters into provider API calls. OpenAI and Anthropic reject these with validation errors, breaking fallback chains. Status: open. Workaround: do not configure per-key TPM/RPM limits on keys where fallback chains must function. Use team-level limits instead.
5. Budget Alerting
5.1 Slack Alerting Configuration
# config.yaml
general_settings:
master_key: sk-1234
alerting: ["slack"]
alerting_threshold: 300 # seconds — alert on slow responses beyond this
spend_report_frequency: "1d" # daily spend digest
alert_types:
- budget_alerts
- spend_reports
- daily_reports
- failed_tracking_spend
alerting_args:
budget_alert_ttl: 86400 # dedup window: 1 alert per key/team per day
report_check_interval: 3600 # background check every hour
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T.../B.../..."
5.2 Route Alert Types to Different Channels
general_settings:
alerting: ["slack"]
alert_to_webhook_url:
budget_alerts: "https://hooks.slack.com/services/.../budget-alerts-channel"
spend_reports: "https://hooks.slack.com/services/.../finops-channel"
daily_reports: "https://hooks.slack.com/services/.../finops-channel"
5.3 Generic Webhook / PagerDuty
LiteLLM does not have a native PagerDuty integration. Use the generic webhook callback and route to PagerDuty via a Webhook→PagerDuty bridge (e.g., AWS EventBridge, Zapier, or a thin Lambda):
general_settings:
alerting: ["webhook"]
environment_variables:
WEBHOOK_URL: "https://events.pagerduty.com/integration/<key>/enqueue"
Webhook payload events include: budget_crossed, threshold_crossed, projected_limit_exceeded.
5.4 Team Soft Budget Email
Configured directly on the team object (see §2.3). Requires SendGrid, Resend, or SMTP integration. No additional config.yaml entry needed once email is wired.
6. Prometheus Budget Metrics
6.1 Enable Prometheus
litellm_settings:
callbacks: ["prometheus"]
success_callback: ["prometheus"]
failure_callback: ["prometheus"]
prometheus_initialize_budget_metrics: true # cron every 5 min for all keys/teams
Multi-worker deployments require:
export PROMETHEUS_MULTIPROC_DIR="/tmp/prometheus_multiproc"
Metrics endpoint: http://localhost:4000/metrics
6.2 Budget Metric Inventory
| Metric | Labels | Description |
|---|---|---|
litellm_remaining_team_budget_metric |
team, team_alias |
Remaining USD budget for team |
litellm_team_max_budget_metric |
team, team_alias |
Configured max budget |
litellm_team_budget_remaining_hours_metric |
team, team_alias |
Hours until budget window resets |
litellm_remaining_api_key_budget_metric |
hashed_api_key, api_key_alias |
Remaining USD for virtual key |
litellm_api_key_max_budget_metric |
hashed_api_key, api_key_alias |
Key max budget |
litellm_api_key_budget_remaining_hours_metric |
hashed_api_key, api_key_alias |
Hours until key window resets |
litellm_spend_metric |
end_user, hashed_api_key, api_key_alias, model, team, team_alias, user |
Running spend counter |
6.3 Prometheus Alert Rules (PromQL)
# prometheus-alerts.yaml
groups:
- name: litellm_budget
rules:
# Team at or above 80% of budget
- alert: LiteLLMTeamBudgetWarning
expr: |
(litellm_team_max_budget_metric - litellm_remaining_team_budget_metric)
/ litellm_team_max_budget_metric > 0.80
for: 5m
labels:
severity: warning
annotations:
summary: "Team {{ $labels.team_alias }} at {{ $value | humanizePercentage }} of budget"
# Team budget exhausted
- alert: LiteLLMTeamBudgetExhausted
expr: litellm_remaining_team_budget_metric <= 0
for: 1m
labels:
severity: critical
annotations:
summary: "Team {{ $labels.team_alias }} budget exhausted"
# Key at or above 90% of budget
- alert: LiteLLMKeyBudgetWarning
expr: |
(litellm_api_key_max_budget_metric - litellm_remaining_api_key_budget_metric)
/ litellm_api_key_max_budget_metric > 0.90
for: 5m
labels:
severity: warning
annotations:
summary: "API key {{ $labels.api_key_alias }} near budget limit"
# Budget window expiring within 6 hours with spend > 50%
- alert: LiteLLMTeamBudgetWindowExpiringSoon
expr: |
litellm_team_budget_remaining_hours_metric < 6
and
(litellm_team_max_budget_metric - litellm_remaining_team_budget_metric)
/ litellm_team_max_budget_metric > 0.50
labels:
severity: warning
annotations:
summary: "Team {{ $labels.team_alias }} window expires in {{ $value }}h with >50% spent"
7. Budget Governance at Scale (50+ Keys / 10+ Teams)
7.1 Governance Architecture Pattern
For Fortune 500 deployments, the recommended hierarchy is:
Cost Center / BU → LiteLLM Team (shared $budget, shared TPM/RPM)
Product/Service → Tag (cross-cutting cost tracking)
Application Instance → Virtual Key (fine-grained key per deployment)
End User → user= param (tenant-level cap via max_end_user_budget)
Tags are additive to teams: a key can belong to team “data-science” AND carry tag “project-ragbot”. If either budget is exhausted, requests block.
7.2 Tag-Based Cross-Cutting Budgets
# Create tag budget for a project spanning multiple teams
curl -X POST 'http://0.0.0.0:4000/tag/new' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"name": "project-ragbot",
"max_budget": 1000.0,
"budget_duration": "30d"
}'
# Assign tag to keys across teams
curl -X POST 'http://0.0.0.0:4000/key/generate' \
-d '{"team_id": "...", "tags": ["project-ragbot", "cost-center-engineering"]}'
7.3 Terraform / IaC Provisioning
There is no official LiteLLM Terraform provider. Multiple community providers exist on the Terraform Registry:
| Provider | Maintainer | Last updated |
|---|---|---|
ncecere/litellm |
Independent, tracks LiteLLM versions | Jan 2026 rewrite |
vCra/litellm |
Independent | Feb 2026 (v1.2.0) |
scalepad/litellm |
Independent | 2025 |
gzamboni/litellm |
Independent | 2025 |
None are officially endorsed by BerriAI. For enterprise IaC, treat these providers as thin API wrappers and validate against LiteLLM’s /key/info and /team/info after apply. Consider writing your own provider using the REST API directly if long-term stability is required.
Example (ncecere/litellm pattern):
terraform {
required_providers {
litellm = {
source = "ncecere/litellm"
version = "~> 1.86"
}
}
}
resource "litellm_team" "data_science" {
team_alias = "data-science"
max_budget = 2000.0
budget_duration = "30d"
tpm_limit = 5000000
rpm_limit = 3000
}
resource "litellm_key" "ds_prod" {
key_alias = "ds-prod-app"
team_id = litellm_team.data_science.id
max_budget = 500.0
budget_duration = "30d"
tpm_limit = 100000
rpm_limit = 300
}
7.4 API-Driven Bulk Key Provisioning
There is no native bulk /key/generate endpoint (single key per call). For 50+ keys, script a loop against the REST API:
import httpx, asyncio
PROXY_URL = "http://localhost:4000"
MASTER_KEY = "sk-1234"
async def provision_keys(team_id: str, count: int, config: dict):
async with httpx.AsyncClient() as client:
tasks = [
client.post(
f"{PROXY_URL}/key/generate",
headers={"Authorization": f"Bearer {MASTER_KEY}"},
json={"team_id": team_id, **config}
)
for i in range(count)
]
results = await asyncio.gather(*tasks)
return [r.json()["key"] for r in results]
# Provision 20 keys for a team
asyncio.run(provision_keys(
team_id="de35b29e-...",
count=20,
config={
"max_budget": 100.0,
"budget_duration": "30d",
"tpm_limit": 50000,
"tags": ["batch-provisioned"]
}
))
7.5 Key Rotation Without Interruption (Enterprise)
LiteLLM Enterprise supports zero-downtime rotation via a grace period:
curl -X POST 'http://localhost:4000/key/sk-abc123/regenerate' \
-H 'Authorization: Bearer sk-1234' \
-d '{"duration": "30d", "grace_period": "24h"}'
During the grace period, both old and new keys are valid. Clients can rotate without a hard cutover window. Grace period values use the same duration string format as budget_duration.
For open-source deployments, key rotation requires deleting and re-creating the key, with a brief period where both keys are valid if you provision the new key before deleting the old. Issue #14877 (PR, merged) adds scheduled rotation support.
Note: Issue #19445 documents a mismatch between LiteLLM auto-rotation and AWS Secrets Manager when both are active simultaneously — pick one rotation mechanism.
8. Pre-Flight Cost Estimation
8.1 Token Counting Before API Call
from litellm import token_counter
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt_text}
]
input_tokens = token_counter(model="gpt-4o", messages=messages)
print(f"Estimated input tokens: {input_tokens}")
Uses model-specific tokenizers; falls back to tiktoken for unknown models.
8.2 Cost Per Token (Lookup)
from litellm import cost_per_token
prompt_cost, completion_cost = cost_per_token(
model="anthropic/claude-3-5-sonnet-20241022",
prompt_tokens=1000,
completion_tokens=500
)
print(f"Estimated cost: ${prompt_cost + completion_cost:.6f}")
Pricing data is fetched live from api.litellm.ai and can be overridden with custom pricing (see §8.4).
8.3 Pre-Flight Cost Gate Pattern
from litellm import token_counter, cost_per_token
def preflight_cost_check(model: str, messages: list, budget_remaining: float) -> bool:
"""Returns False if estimated cost exceeds remaining budget."""
input_tokens = token_counter(model=model, messages=messages)
# Estimate output tokens (e.g., 25% of context window or known typical output)
estimated_output = min(input_tokens // 4, 2000)
input_cost, output_cost = cost_per_token(
model=model,
prompt_tokens=input_tokens,
completion_tokens=estimated_output
)
estimated_total = input_cost + output_cost
return estimated_total <= budget_remaining
8.4 Post-Call Cost Extraction
from litellm import completion, completion_cost
response = completion(model="gpt-4o", messages=messages)
cost = completion_cost(completion_response=response)
print(f"Actual call cost: ${float(cost):.8f}")
8.5 Model Cost Map
from litellm import model_cost
# Returns dict: model_name → {max_tokens, input_cost_per_token, output_cost_per_token, ...}
sonnet_pricing = model_cost.get("claude-3-5-sonnet-20241022", {})
The LiteLLM proxy also exposes GET /model/info which returns cost data alongside model capabilities.
9. Multi-Model Cost Routing
9.1 Provider Budget Routing (Route Away from Exhausted Providers)
When a provider’s daily/monthly budget is consumed, LiteLLM skips it and routes to the next available provider. This is the primary mechanism for “degrade to cheaper model when budget exhausted”:
# config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4o
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gpt-4o
litellm_params:
model: anthropic/claude-3-haiku-20240307
api_key: os.environ/ANTHROPIC_API_KEY
router_settings:
provider_budget_config:
openai:
budget_limit: 200
time_period: 1d
anthropic:
budget_limit: 500
time_period: 1d
redis_host: os.environ/REDIS_HOST
redis_port: os.environ/REDIS_PORT
redis_password: os.environ/REDIS_PASSWORD
general_settings:
master_key: sk-1234
When OpenAI’s $200/day is consumed, requests to gpt-4o automatically route to Anthropic equivalents. Check /provider/budgets to inspect remaining budget and reset times.
9.2 Cost-Based Routing Strategy
The Router selects the cheapest healthy deployment among a model group:
router_settings:
routing_strategy: "cost-based-routing"
from litellm import Router
router = Router(
model_list=[
{
"model_name": "smart-model",
"litellm_params": {
"model": "openai/gpt-4o",
"input_cost_per_token": 0.0000025,
"output_cost_per_token": 0.00001
}
},
{
"model_name": "smart-model",
"litellm_params": {
"model": "anthropic/claude-3-5-sonnet-20241022",
"input_cost_per_token": 0.000003,
"output_cost_per_token": 0.000015
}
}
],
routing_strategy="cost-based-routing"
)
response = await router.acompletion(
model="smart-model",
messages=[{"role": "user", "content": "..."}]
)
Limitation: Cost-based routing picks the cheapest deployment within a model group. It does not dynamically degrade based on remaining key/team budget. For budget-triggered degradation, combine with provider budget routing or application-layer logic.
9.3 Budget-Triggered Model Degradation (Application Pattern)
LiteLLM does not natively route “use Haiku when team is at 80% of budget.” This requires application logic:
import httpx
def get_model_for_team(team_id: str, base_model: str) -> str:
"""Returns cheaper fallback if team budget is >80% consumed."""
r = httpx.get(
f"http://localhost:4000/team/info?team_id={team_id}",
headers={"Authorization": "Bearer sk-1234"}
).json()
spend = r["team_info"]["spend"]
max_budget = r["team_info"]["max_budget"]
if max_budget and (spend / max_budget) > 0.80:
return FALLBACK_MODELS.get(base_model, base_model)
return base_model
FALLBACK_MODELS = {
"claude-3-5-sonnet-20241022": "claude-3-haiku-20240307",
"gpt-4o": "gpt-4o-mini"
}
Alternatively, use the dynamic_rate_limiter_v3 callback with priority_reservation to guarantee capacity for prod traffic and starve dev traffic when the model is saturated (§4, Dynamic Rate Limits).
9.4 model_group_alias for Cost-Tiered Pools
model_group_alias maps a single alias to a different internal model group, allowing clients to use a stable name while routing to different cost tiers based on configuration:
model_list:
- model_name: premium
litellm_params:
model: openai/gpt-4o
- model_name: standard
litellm_params:
model: openai/gpt-4o-mini
router_settings:
model_group_alias:
"default": "standard" # Default pool for most keys
"premium-tier": "premium" # Alias for high-priority keys
Keys are then generated with {"model": "default"} or {"model": "premium-tier"}, enabling tier assignment without exposing model names to consumers. The hidden: true flag on an alias entry excludes it from the public /model/list endpoint.
10. Known Bugs Summary (as of June 2026)
| Issue | Severity | Description | Status |
|---|---|---|---|
| #27735 | High | Stale Redis reservations trigger false BudgetExceededError on virtual keys |
Open |
| #28146 | High | TPM/RPM-limited keys inject _litellm_* params into provider calls, breaking fallbacks (OpenAI, Anthropic) |
Open — affects v1.85.0 |
| #25495 | Medium | Organization budget_duration does not reset spend after window expires |
Closed (verify fix) |
| #10052 | Medium | Key-level model_max_budget not enforced; TPM limits fire ~50% late in DB mode |
Closed (PRs #10965, #10497) |
| #28235 | Low | Multiple concurrent budget windows (budget_limits array) — feature request / partial impl |
Open |
11. Sources
- Virtual Keys — LiteLLM Docs
- Team Budgets — LiteLLM Docs
- Budgets & Rate Limits (Users) — LiteLLM Docs
- Tag Budgets — LiteLLM Docs
- Provider Budget Routing — LiteLLM Docs
- Budget/Rate Limit Tiers — LiteLLM Docs
- Alerting / Webhooks — LiteLLM Docs
- Team Soft Budget Alerts — LiteLLM Docs
- Prometheus Metrics — LiteLLM Docs
- Dynamic TPM/RPM Allocation — LiteLLM Docs
- Router / Load Balancing — LiteLLM Docs
- Token Usage & Cost — LiteLLM Docs
- Token Counting — LiteLLM Docs
- Pricing Calculator — LiteLLM Docs
- Terraform Provider (ncecere)
- Terraform Provider (scalepad)
- GitHub Issue #10052 — Key-level model limits enforcement
- GitHub Issue #27735 — Stale spend BudgetExceededError
- GitHub Issue #28146 — TPM/RPM params leaking into provider calls
- GitHub Issue #25495 — Organization budget_duration not resetting
- GitHub Issue #28235 — Multiple concurrent budget windows
- GitHub PR #14877 — Scheduled key rotation