{"record":{"id":"fcc84ad1b8cec910","repo":"headroomlabs-ai/headroom","slug":"budget-exceeded-for-budget-period-period","errorCode":null,"errorMessage":"Budget exceeded for {budget_period} period","messagePattern":"Budget exceeded for (.+?) period","errorType":"http","errorClass":"HTTPException","httpStatus":429,"severity":"error","filePath":"headroom/proxy/handlers/anthropic.py","lineNumber":1053,"sourceCode":"                    await self.metrics.record_rate_limited(provider=provider_name)\n                    # Unit 4: release the pre-upstream semaphore before we\n                    # bail out of the handler via HTTPException — FastAPI's\n                    # exception handler will NOT run our ``finally``.\n                    await _finalize_pre_upstream()\n                    raise HTTPException(\n                        status_code=429,\n                        detail=f\"Rate limited. Retry after {wait_seconds:.1f}s\",\n                        headers={\"Retry-After\": str(int(wait_seconds) + 1)},\n                    )\n\n            # Budget check\n            if self.cost_tracker:\n                allowed, remaining = self.cost_tracker.check_budget()\n                if not allowed:\n                    # Unit 4: release the pre-upstream semaphore before we\n                    # bail out of the handler via HTTPException.\n                    await _finalize_pre_upstream()\n                    raise HTTPException(\n                        status_code=429,\n                        detail=self.cost_tracker.budget_denial_detail(),\n                    )\n\n            # Memory: Get user ID when memory is enabled (fallback to \"default\" for simple DevEx).\n            # Reads `request.headers` directly because the local `headers` dict was\n            # stripped of `x-headroom-*` above for the upstream-bound copy (PR-A5).\n            memory_user_id: str | None = None\n            memory_request_ctx = None\n            if self.memory_handler:\n                memory_user_id = request.headers.get(\n                    \"x-headroom-user-id\",\n                    os.environ.get(\"USER\", os.environ.get(\"USERNAME\", \"default\")),\n                )\n                # Per-project memory routing (GH #462). Build the context\n                # once here so save / search / inject all resolve against\n                # the same workspace. Tier order: explicit project-id /\n                # cwd headers → CLI override → system prompt env block.","sourceCodeStart":1035,"sourceCodeEnd":1071,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/proxy/handlers/anthropic.py#L1035-L1071","documentation":"After rate limiting, the Anthropic handler asks the cost tracker whether the period budget still has headroom; when check_budget() returns not-allowed the handler releases the pre-upstream semaphore and returns 429 with a detail string built by cost_tracker.budget_denial_detail() — which names the budget period, spend breakdown, and whether enforcement was blocked because part of the spend was booked from Headroom's own token estimate (when the provider returned no usage data). This is a spend-control refusal, not throttling: retrying immediately will keep failing.","triggerScenarios":"The configured spend limit for the current budget period (e.g. daily/monthly) has been reached or exceeded, and a new /v1/messages request arrives; or the budget-basis policy blocks when a request's cost can only be estimated because the provider returned no usage.","commonSituations":"Long-running agents exhausting a daily cap mid-run; a budget set too low after a model-price change; estimated-cost booking (missing usage in provider responses) pushing measured spend to the limit under a strict basis policy; forgotten budget config left from a test.","solutions":["Read the full denial detail — if it says enforcement was blocked on estimated cost, fix usage reporting (or relax the basis policy) rather than raising the budget.","Raise the period budget limit if the spend is legitimate.","Wait for the period to roll over (daily budgets reset at the period boundary), or split the key/workload across budgets."],"exampleFix":"# before\nbudget_limit_usd = 5.0  # agent burns through it mid-run -> 429\n\n# after\nbudget_limit_usd = 25.0  # sized to actual token volume; or reset at period boundary","handlingStrategy":"fallback","validationCode":"# Before long runs, query the cost tracker's remaining budget if exposed:\n# allowed, remaining = cost_tracker.check_budget()\n# skip or degrade (smaller model, less context) when remaining < estimated_request_cost","typeGuard":null,"tryCatchPattern":"resp = await client.post(\"/v1/messages\", json=body)\nif resp.status_code == 429 and \"Budget\" in resp.json()[\"detail\"]:\n    notify_operator(f\"budget exhausted: {resp.json()['detail']}\")\n    switch_to_budgeted_fallback_model()  # or pause the batch until period reset","preventionTips":["Size period budgets from measured token spend, revisiting after model/price changes.","Alert on spend > 80% of budget so exhaustion never surprises mid-run.","Understand the basis policy: strict blocking on estimated cost needs usage reporting to work, not a bigger budget."],"tags":["budget","cost-control","http-429","proxy","spend-limit"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}