litellm - ✅(Solved) Fix max_end_user_budget_id does not persist budget_id to DB — budget reset never runs for auto-created end users [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#25386Fetched 2026-04-09 07:52:22
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Participants
Timeline (top)
cross-referenced ×1referenced ×1

Root Cause

Three code paths are involved:

  1. Auth-time budget application (auth_checks.py_apply_default_budget_to_end_user()): When a request arrives with an end-user ID, this function merges the default budget onto the in-memory LiteLLM_EndUserTable object by setting end_user_obj.litellm_budget_table. The budget_id column is never written to the DB.

  2. End user auto-creation (utils.pyupdate_end_user_spend()): The upsert create clause only sets user_id, spend, and blocked. It does not include budget_id, even when litellm.max_end_user_budget_id is configured. So new end users are created with budget_id = NULL.

  3. Budget reset job (reset_budget_job.pyreset_budget_for_litellm_budget_table()): This job finds budgets past their reset_at date, then queries end users with WHERE budget_id IN (...). Since auto-created end users have budget_id = NULL, they are never matched by this query and their spend is never reset.

Fix Action

Fixed

PR fix notes

PR #25387: fix: persist budget_id to DB for auto-created end users using max_end_user_budget_id

Description (problem / solution / changelog)

Summary

Fixes #25386

When max_end_user_budget_id is configured, end users created via the x-litellm-end-user-id header have their default budget applied only in-memory at auth time. The budget_id is never written to the LiteLLM_EndUserTable row in the database, so:

  • /customer/info shows litellm_budget_table: null
  • The budget reset job (WHERE budget_id IN (...)) never finds these users
  • Spend accumulates forever and never resets, eventually permanently blocking users

Changes

  1. litellm/proxy/utils.py — Include budget_id in the upsert create clause in update_end_user_spend() when max_end_user_budget_id is configured. Only affects new user creation — existing explicit budget assignments are never overwritten.

  2. litellm/proxy/auth/auth_checks.py — Lazy backfill in _apply_default_budget_to_end_user(): when applying the in-memory default budget, if the user's DB record has budget_id = NULL, persist it. This self-heals existing users on their next request without requiring a migration.

  3. litellm/proxy/_types.py — Add budget_id: Optional[str] = None to LiteLLM_EndUserTable Pydantic model to match the Prisma schema.

Test plan

  • New end user created via x-litellm-end-user-id with max_end_user_budget_id configured → verify budget_id is set in DB
  • Existing end user with budget_id = NULL makes a request → verify budget_id gets backfilled
  • End user with explicitly assigned budget → verify it is NOT overwritten by the default
  • Budget reset job runs → verify it now finds and resets spend for auto-created users
  • /customer/info reflects the assigned budget after fix

Changed files

  • litellm/proxy/_types.py (modified, +1/-0)
  • litellm/proxy/auth/auth_checks.py (modified, +20/-3)
  • litellm/proxy/utils.py (modified, +8/-5)
RAW_BUFFERClick to expand / collapse

Bug Description

When using max_end_user_budget_id in the proxy config to set a default budget for end users, the budget is only applied in-memory at auth time but never written to the LiteLLM_EndUserTable database record. This causes the budget reset job to skip these users entirely — their spend accumulates forever and never resets.

Root Cause

Three code paths are involved:

  1. Auth-time budget application (auth_checks.py_apply_default_budget_to_end_user()): When a request arrives with an end-user ID, this function merges the default budget onto the in-memory LiteLLM_EndUserTable object by setting end_user_obj.litellm_budget_table. The budget_id column is never written to the DB.

  2. End user auto-creation (utils.pyupdate_end_user_spend()): The upsert create clause only sets user_id, spend, and blocked. It does not include budget_id, even when litellm.max_end_user_budget_id is configured. So new end users are created with budget_id = NULL.

  3. Budget reset job (reset_budget_job.pyreset_budget_for_litellm_budget_table()): This job finds budgets past their reset_at date, then queries end users with WHERE budget_id IN (...). Since auto-created end users have budget_id = NULL, they are never matched by this query and their spend is never reset.

Impact

  • All end users created via x-litellm-end-user-id / x-litellm-customer-id headers have budget_id = NULL in the database
  • Budget enforcement works on the first cycle (in-memory at auth time), but spend never resets
  • After the first budget period, users are permanently blocked once they hit the limit
  • In our environment: 121,049 of 121,050 end users had budget_id = NULL, and one tenant accumulated $646 in spend that never reset

Expected Behavior

When max_end_user_budget_id is configured and a new end user is auto-created, the budget_id should be persisted to the database so that:

  1. /customer/info reflects the assigned budget
  2. The budget reset job correctly finds and resets spend for these users

Suggested Fix

  1. In update_end_user_spend() (utils.py), include budget_id: litellm.max_end_user_budget_id in the upsert create clause when configured
  2. In _apply_default_budget_to_end_user() (auth_checks.py), backfill budget_id to the DB for existing users where it's currently NULL

Environment

  • LiteLLM version: v1.81.12
  • Database: PostgreSQL via Prisma
  • Config: max_end_user_budget_id set in proxy config YAML

Reproduction

  1. Configure max_end_user_budget_id pointing to a budget with budget_duration: "daily"
  2. Send requests with x-litellm-end-user-id: test-user-123
  3. Query the database: SELECT budget_id FROM "LiteLLM_EndUserTable" WHERE user_id = 'test-user-123' → returns NULL
  4. Wait for budget reset job to run → user's spend is not reset
  5. Call /customer/info?end_user_id=test-user-123litellm_budget_table: null

extent analysis

TL;DR

Update the update_end_user_spend() function in utils.py to include budget_id in the upsert create clause and backfill budget_id to the DB for existing users in _apply_default_budget_to_end_user().

Guidance

  • Modify the update_end_user_spend() function to include budget_id: litellm.max_end_user_budget_id in the upsert create clause when max_end_user_budget_id is configured.
  • Update the _apply_default_budget_to_end_user() function to persist the budget_id to the database for existing users where it's currently NULL.
  • Verify the fix by checking the LiteLLM_EndUserTable database record for newly created end users and ensuring that the budget_id is correctly set.
  • Test the budget reset job to confirm that it correctly resets spend for end users with a persisted budget_id.

Example

# In utils.py
def update_end_user_spend(...):
    # ...
    if litellm.max_end_user_budget_id:
        upsert_clause = {
            # ...
            'budget_id': litellm.max_end_user_budget_id,
        }
    # ...

# In auth_checks.py
def _apply_default_budget_to_end_user(...):
    # ...
    if litellm.max_end_user_budget_id and not end_user_obj.litellm_budget_table:
        # Persist budget_id to the DB
        end_user_obj.litellm_budget_table = litellm.max_end_user_budget_id
        # ...

Notes

This fix assumes that the max_end_user_budget_id is correctly configured and that the litellm.max_end_user_budget_id value is accessible in the utils.py and auth_checks.py modules.

Recommendation

Apply the suggested fix to update the update_end_user_spend() and _apply_default_budget_to_end_user() functions to persist the budget_id to the database for existing and new end users. This will ensure that the budget reset job correctly resets spend for end users with a persisted budget_id.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING