Skip to content

fix(health): return 503 when targeted model is unhealthy or DB is disconnected - #27003

Merged
ryan-crabbe-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_health-endpoint-non200-on-failure
May 1, 2026
Merged

fix(health): return 503 when targeted model is unhealthy or DB is disconnected#27003
ryan-crabbe-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_health-endpoint-non200-on-failure

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • GET /health?model=foo (and ?model_id=foo) now returns HTTP 503 instead of 200 when the targeted model has zero healthy endpoints. Body shape is unchanged.
  • This applies to both the live health-check path and the background-cache path: the cache is filtered to the targeted deployment IDs before healthy_count is evaluated, so an unhealthy foo is no longer masked by other healthy models in the global aggregate.
  • GET /health/readiness now returns HTTP 503 instead of 200 when a Prisma DB is configured but its connection check fails. The prisma_client is None ("Not connected") path stays 200, since "no DB configured" is a valid deployment.
  • This is a backwards-incompatible change for callers that asserted status == 200 on /health* regardless of body content, but that's fine — the new behavior is what monitoring systems and orchestrators expect.

Resolves LIT-2552

Test plan

  • uv run pytest tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py — 7 new tests (4 for /health model param incl. the background-cache + targeted case, 3 for /health/readiness DB states); pre-existing tests updated to pass model=None, model_id=None explicitly when calling the handler directly so the FastAPI Query(None) sentinel doesn't leak into the new cache filter.
  • uv run mypy litellm/proxy/health_endpoints/_health_endpoints.py — clean.
  • Live before/after demo with bad-API-key model: BEFORE HTTP 200, AFTER HTTP 503, body identical.

…or DB is disconnected

/health?model=foo and /health?model_id=foo previously returned HTTP 200
even when zero endpoints were healthy, forcing monitoring systems to
parse the JSON body to detect failure. /health/readiness similarly
returned 200 even when a configured Prisma DB was unreachable, leaving
unhealthy pods in rotation.

Both endpoints now flip to HTTP 503 in the failure case while keeping
the JSON response body identical, so existing parsers continue to work
and orchestrators can rely on the HTTP status alone.
@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes /health?model=foo return HTTP 503 (instead of 200) when the targeted model has zero healthy endpoints, and makes /health/readiness return 503 when a configured Prisma DB is unreachable. It also introduces _resolve_targeted_model_ids to correctly scope the background-cache result to the requested deployment before evaluating healthy_count, and fixes the pre-existing authorization gap where a scoped caller could read any deployment's cache entry by guessing its ID.

The only remaining issues are two new test functions that omit the model_id=None sentinel workaround documented throughout this PR's own test updates, and an inaccurate inline comment claiming Response() initialises to None (it initialises to 200 in Starlette).

Confidence Score: 4/5

Safe to merge; only P2 test-consistency issues remain.

All production logic changes look correct. The only findings are P2: two new tests omit an explicit model_id=None (they pass today by coincidence) and one has a misleading comment. No P0/P1 issues found beyond what was already raised in prior review threads.

tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py — two tests need model_id=None added for consistency with the sentinel pattern.

Important Files Changed

Filename Overview
litellm/proxy/health_endpoints/_health_endpoints.py Adds _resolve_targeted_model_ids to scope background-cache results to a specific deployment before evaluating healthy_count; adds 503 in _post_process for targeted-unhealthy paths and in health_readiness for DB disconnection. Logic is correct for the stated goals.
tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py 7 new tests covering targeted-503, cache scoping, and readiness DB states. Two new 200-path tests omit model_id=None and one has an inaccurate comment about Response() initialization; tests still pass by coincidence today.

Reviews (3): Last reviewed commit: "fix(health): validate model_id against s..." | Re-trigger Greptile

Comment thread litellm/proxy/health_endpoints/_health_endpoints.py
Comment on lines +1441 to +1442
if db_health_status["status"] != "connected":
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Backwards-incompatible HTTP status change without a user-controlled flag

Both /health?model=foo and /health/readiness now return 503 where they previously returned 200. The PR description acknowledges this breaks existing callers. Per the team's rule, backwards-incompatible behavioural changes must be gated behind a user-controlled flag (e.g., litellm.health_strict_status_codes) so that operators can opt in on their own schedule rather than being broken on upgrade. The same applies to the /health 503 change introduced in _post_process.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment thread tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py
…503 check

When use_background_health_checks is enabled, /health?model=foo returned
the full cached aggregate across every model — so an unhealthy foo
combined with any other healthy deployment kept healthy_count > 0 and
the targeted-503 path never fired.

Resolve the targeted model/model_id to a deployment-id set first
(mirroring perform_health_check's match-on-model_name-or-litellm_model
semantics) and narrow the cache to those IDs before _post_process
evaluates healthy_count, so the 503 contract holds for both the live
and cache code paths.
Per review: `assert response.status_code != 503` is satisfied by 404,
500, or any other non-503 code, so a regression that returned the wrong
non-503 status would slip through. Switch to `== 200` so the assertions
verify the actual expected status, not just the absence of one specific
failure.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

Comment thread litellm/proxy/health_endpoints/_health_endpoints.py Outdated
…h resolver

A non-admin scoped to ["model-a"] could call /health?model_id=id-b
(where id-b belongs to a deployment outside their scope) and the
background-cache code path would return id-b's cached health entry. The
helper returned {model_id} unconditionally, so the cache filter was
driven by an unvalidated id and the global cache leaked the entry — the
ternary `targeted_ids if not None else allowed_model_ids` skipped any
intersection with the caller's allowed deployments.

Make _resolve_targeted_model_ids walk the supplied model_list for both
the model and model_id branches. Callers pass an already-scoped list
(filtered to allowed model_names for non-admins, full list for admins),
so an out-of-scope model_id resolves to an empty set and the cache
filter drops every entry — matching the live path's existing behavior.
@ryan-crabbe-berri

ryan-crabbe-berri commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

The P1 backwards-incompat-without-feature-flag finding is intentionally not addressed — accepted in the PR description.

@ryan-crabbe-berri
ryan-crabbe-berri merged commit 610f79d into litellm_internal_staging May 1, 2026
114 of 115 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_health-endpoint-non200-on-failure branch May 1, 2026 21:23
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…-non200-on-failure

fix(health): return 503 when targeted model is unhealthy or DB is disconnected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants