fix(proxy): invalidate cached project object on project update and delete - #36028
Merged
ryan-crabbe-berri merged 4 commits intoAug 7, 2026
Conversation
…project/delete
The auth path reads projects cache-first via get_project_object with a 60s
TTL and no freshness check, but no project write endpoint ever evicted the
project_id:{id} cache entry. A project cached before /project/update added a
model allowlist kept an empty models list in cache, so _run_project_checks
skipped can_project_access_model and project-bound keys could call team
models outside the project allowlist until the TTL expired. The same
staleness applied to blocked status and budget fields, and /project/delete
left the deleted project enforceable from cache.
Evict the cache entry after the DB write in update_project and
delete_project via a shared delete_cached_project_object helper, with the
cache key derivation shared with get_project_object.
yuneng-berri
approved these changes
Aug 6, 2026
Contributor
Greptile SummaryThis PR invalidates cached project objects after project updates and deletions, and broadcasts invalidations so each worker removes its local cached copy
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| Filename | Overview |
|---|---|
| enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py | Project update and delete now evict the affected cached project after the database mutation |
| litellm/proxy/auth/auth_checks.py | Project cache keys are centralized and project eviction tolerates cache failures before broadcasting invalidation |
| litellm/proxy/common_utils/auth_cache_invalidation_pubsub.py | Adds namespaced best-effort invalidation publication and worker-local cache eviction through Redis pub/sub |
| litellm/proxy/proxy_server.py | Integrates the invalidation subscriber with proxy startup and shutdown |
| tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py | Covers update and delete eviction, cache-backend failure tolerance, and invalidation publication |
| tests/test_litellm/proxy/auth/test_auth_checks.py | Verifies project-level model restrictions remain enforced when the key allowlist is empty |
| tests/test_litellm/proxy/common_utils/test_auth_cache_invalidation_pubsub.py | Covers publication, namespacing, malformed messages, Redis failures, and subscriber-driven local eviction |
Reviews (3): Last reviewed commit: "fix(lint): sort auth cache invalidation ..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…ake eviction best-effort Single-worker eviction leaves every other worker serving its in-memory copy of the mutated project until the 60s TTL expires, so a project allowlist change was still bypassable on multi-worker deployments. Add a coordination Redis pub/sub channel (litellm_proxy.auth_cache_invalidation): project eviction publishes the cache key and a per-worker subscriber deletes the local in-memory entry, with the next auth read refetching from the DB. Subscriber starts on any deployment with a coordination Redis and falls back to the TTL when none is configured. Also wrap the eviction in a best-effort catch: the DB write has already committed when eviction runs, so a cache backend error must not turn a successful update into a 500 or abort the remaining ids in /project/delete.
Contributor
Author
Contributor
…rt shutdown catch The strict-budget gate flagged the new import block as un-sorted (I001) and the broad except in stop_auth_cache_invalidation_subscriber (BLE001); the catch is intentional since a failing stop must not break proxy shutdown, so it carries a named suppression instead of counting against the budget.
Contributor
Author
|
@greptileai re review update score |
ryan-crabbe-berri
enabled auto-merge (squash)
August 6, 2026 16:59
ryan-crabbe-berri
merged commit Aug 7, 2026
83ab6e0
into
litellm_internal_staging
78 of 79 checks passed
ryan-crabbe-berri
deleted the
litellm_lit_3803_project_cache_invalidation
branch
August 7, 2026 15:19
5 tasks
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
How it solves it:
Relevant issues
Linear ticket
Resolves LIT-3803
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
All runs are against live local proxies backed by Postgres, making real Groq calls. Setup: a team with two models, a project under it created with no allowlist, and a virtual key bound to the project with no models assigned
Single worker: stale cache bypass
The repro sequence is: make one completion with the key so auth caches the project (still no allowlist), then add the allowlist, then immediately call a team model outside it
Before, with the un-fixed endpoints (parent commit c2c795f), the out-of-allowlist call went straight through to the provider:
After (70b3ed3), the same sequence is blocked immediately:
Multi worker: cross-worker broadcast
Two proxies (A on :4173, B on :4174) share the same Postgres and a coordination Redis (
general_settings.coordination_redis). Both warm their local project cache with one completion each while the allowlist is still empty, the allowlist is added via A, and the out-of-allowlist model is called via B immediately afterBefore (70b3ed3, eviction without broadcast), worker B kept serving its stale in-memory copy and the call went through to the provider:
After (ad00a37), worker B blocks immediately because the broadcast evicted its local copy:
and the allowed model still completes for real via B:
The enforcement itself lives in
common_checksin the shared auth layer, upstream of any specific LLM endpoint, so the same 403 applies to /v1/chat/completions, /v1/messages, and /v1/responses alikeType
🐛 Bug Fix
Changes
Auth reads projects through
get_project_object, which servesproject_id:{id}fromuser_api_key_cachewith a 60s TTL and no freshness check, and nothing ever evicted that entry: neither /project/update nor /project/delete touched the cache. A project cached while itsmodelslist was still empty (warmed during key creation or by any completion made before the admin set the allowlist) kept an empty allowlist in cache, so_run_project_checksskippedcan_project_access_modeland a project-bound key could call team models outside the project allowlist until the TTL expired. This is the bypass reported in the ticket. Blocked status and budget fields had the same staleness window, and a deleted project stayed enforceable from cacheThe fix adds
delete_cached_project_objectnext toget_project_objectinauth_checks.py, shares the cache-key derivation between the two via_project_cache_keyso they can't drift, and calls the eviction after the DB write inupdate_projectanddelete_project.UserApiKeyCacheis aDualCache, so the eviction clears both the in-memory layer and Redis where configuredSince
DualCachereads return local in-memory hits before consulting Redis, evicting locally plus Redis still leaves every other worker serving its own stale copy. The newauth_cache_invalidation_pubsubmodule closes that: eviction publishes the cache key on a coordination Redis channel (litellm_proxy.auth_cache_invalidation, namespace-aware, following the existingconfig_sync_pubsubpattern), and anAuthCacheInvalidationSubscriberon every worker deletes the local in-memory entry so the next auth read refetches from the DB. The subscriber starts whenever a coordination Redis is configured, independent ofstore_model_in_db, and deployments without Redis fall back to the TTL exactly as beforeEviction and publish are both best-effort: the DB write has already committed when they run, so a cache backend error logs a warning instead of turning a successful update into a 500 or aborting the remaining ids in /project/delete
Tests: two endpoint regressions prove update and delete evict the cached project (both fail without the fix), the enforcement test the ticket asked for proves a key with
models: []passes the key layer unrestricted while_run_project_checksreturns 403project_model_access_denied, pub/sub tests cover publish (channel, namespacing, no-redis no-op, error swallowing) and the subscriber (deletes the local entry on message, ignores malformed messages), and endpoint tests prove update survives a failing cache backend and eviction publishes the invalidationFinal Attestation