Skip to content

fix: /health/readiness 503 loop when DB is unreachable - #26134

Merged
ishaan-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_fix-health-readiness-503
Apr 20, 2026
Merged

fix: /health/readiness 503 loop when DB is unreachable#26134
ishaan-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_fix-health-readiness-503

Conversation

@ishaan-berri

Copy link
Copy Markdown
Contributor

Relevant issues

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🐛 Bug Fix

Changes

/health/readiness was returning 503 Service Unhealthy (All connection attempts failed) / Service Unhealthy (Client is not connected to the query engine...) every 1-2 hours with no recovery.

Root cause: _db_health_readiness_check() catches the Prisma exception, then calls handle_db_exception(e) which re-raises it (unless allow_requests_on_db_unavailable=True, which is almost never set). The re-raise escapes the function, hits the outer handler in health_readiness(), and becomes a 503. The reconnect code below it was never reached.

Two changes in _db_health_readiness_check:

  1. Remove handle_db_exception(e) — that helper enforces the allow_requests_on_db_unavailable flag for API request handlers. The health endpoint just needs to report DB state.

  2. Replace raw disconnect() + connect() inline reconnect with attempt_db_reconnect() — the inline version bypassed the reconnect lock, cooldown, escalation, and heavy-reconnect (recreate_prisma_client) path.

Manual Test Results

Tested against both error types seen in prod:

Before fix — exception escapes, would become 503:

OLD | httpx.ConnectError (All connection attempts failed): RAISED -> ConnectError: All connection attempts failed
OLD | ClientNotConnectedError: RAISED -> ClientNotConnectedError: Client is not connected to the query engine...

After fix — returns disconnected status, no 503:

NEW | httpx.ConnectError (All connection attempts failed): returned disconnected
NEW | ClientNotConnectedError: returned disconnected

Recovery path — reconnect succeeds, next call returns connected:

httpx.ConnectError: returned connected
ClientNotConnectedError: returned connected

Unit tests in tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py — 24 passed (3 pre-existing errors unrelated to this change, require prisma generate).

yuneng-berri and others added 3 commits April 18, 2026 19:33
…le_db_exception re-raising

handle_db_exception() re-raises the Prisma exception inside _db_health_readiness_check's
except block, which propagates out to health_readiness() and gets wrapped in a 503.
The health endpoint never reached the reconnect path and the service never recovered.

Fix:
- Remove handle_db_exception() call from _db_health_readiness_check — that helper is
  for API request handlers (allow_requests_on_db_unavailable flag), not health checks
- Replace raw disconnect()+connect() with attempt_db_reconnect(), which uses the proper
  lock, cooldown, escalation, and heavy-reconnect (recreate_prisma_client) machinery
- Remove tests that expected handle_db_exception to re-raise (old buggy behaviour)
- Remove tests asserting disconnect()/connect() calls (replaced by attempt_db_reconnect)
- Add regression tests covering the 503 loop fix:
  - transport errors never raise (ClientNotConnectedError, httpx.ConnectError, etc.)
  - reconnect success path returns 'connected'
  - reconnect failure path returns 'disconnected' without raising
  - non-transport errors return 'disconnected', skip reconnect
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ yuneng-berri
❌ ishaan-berri
You have signed the CLA already but the status is still pending? Let us recheck it.

@ishaan-berri
ishaan-berri temporarily deployed to integration-postgres April 20, 2026 20:41 — with GitHub Actions Inactive
@ishaan-berri
ishaan-berri temporarily deployed to integration-postgres April 20, 2026 20:41 — with GitHub Actions Inactive
@gitguardian

gitguardian Bot commented Apr 20, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
29203065 Triggered JSON Web Token 26fcbc9 tests/test_litellm/proxy/test_litellm_pre_call_utils.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@greptile-apps

greptile-apps Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a /health/readiness 503 infinite loop by removing handle_db_exception(e) (which re-raised exceptions when allow_requests_on_db_unavailable=False) and replacing the bare disconnect()/connect() inline reconnect with attempt_db_reconnect(), which respects the reconnect lock, cooldown, and escalation path. The test suite is refreshed to validate that transport errors now always return {"status": "disconnected"} rather than propagating an exception.

Confidence Score: 5/5

Safe to merge — the fix is minimal, well-understood, and all findings are P2 or lower.

The root cause is clearly identified and the two-line production change is the correct minimal fix. Test coverage is comprehensive (never-raise regression, reconnect-succeeds, reconnect-fails, non-transport-error paths). No custom rules are violated. The only note is that lock_timeout_seconds is not forwarded to attempt_db_reconnect, which could theoretically block the health endpoint while a concurrent reconnect holds the lock, but this is covered by the cooldown check and is consistent with how other callers invoke the same method.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/health_endpoints/_health_endpoints.py Removes the re-raising handle_db_exception call and replaces bare disconnect/connect with attempt_db_reconnect, correctly preventing 503 loops on DB outages.
tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py Old tests that expected re-raise behavior (the bug) are replaced with regression tests that assert the function always returns a dict and calls attempt_db_reconnect appropriately.

Sequence Diagram

sequenceDiagram
    participant R as /health/readiness
    participant H as _db_health_readiness_check
    participant PC as PrismaClient
    participant AR as attempt_db_reconnect

    R->>H: call
    H->>PC: health_check()
    PC-->>H: raises ConnectError / ClientNotConnectedError

    Note over H: BEFORE fix: handle_db_exception re-raised<br/>→ exception escaped → 503
    Note over H: AFTER fix: exception caught, cache=disconnected

    H->>H: is_database_transport_error(e)?
    alt transport error
        H->>AR: attempt_db_reconnect(reason="health_readiness_check")
        AR-->>H: True / False (never raises)
        alt reconnect succeeded
            H->>PC: health_check()
            PC-->>H: OK
            H-->>R: {"status": "connected"}
        else reconnect failed
            H-->>R: {"status": "disconnected"}
        end
    else non-transport error
        H-->>R: {"status": "disconnected"}
    end
Loading

Reviews (1): Last reviewed commit: "test: update health readiness tests for ..." | Re-trigger Greptile

@yuneng-berri
yuneng-berri self-requested a review April 20, 2026 22:28
@ishaan-berri
ishaan-berri merged commit 9aee0da into litellm_internal_staging Apr 20, 2026
127 of 135 checks passed
@ishaan-berri
ishaan-berri deleted the litellm_fix-health-readiness-503 branch April 20, 2026 22:29
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
* fix: /health/readiness returns 503 when DB is unreachable due to handle_db_exception re-raising

handle_db_exception() re-raises the Prisma exception inside _db_health_readiness_check's
except block, which propagates out to health_readiness() and gets wrapped in a 503.
The health endpoint never reached the reconnect path and the service never recovered.

Fix:
- Remove handle_db_exception() call from _db_health_readiness_check — that helper is
  for API request handlers (allow_requests_on_db_unavailable flag), not health checks
- Replace raw disconnect()+connect() with attempt_db_reconnect(), which uses the proper
  lock, cooldown, escalation, and heavy-reconnect (recreate_prisma_client) machinery

* test: update health readiness tests for handle_db_exception removal

- Remove tests that expected handle_db_exception to re-raise (old buggy behaviour)
- Remove tests asserting disconnect()/connect() calls (replaced by attempt_db_reconnect)
- Add regression tests covering the 503 loop fix:
  - transport errors never raise (ClientNotConnectedError, httpx.ConnectError, etc.)
  - reconnect success path returns 'connected'
  - reconnect failure path returns 'disconnected' without raising
  - non-transport errors return 'disconnected', skip reconnect

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
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.

3 participants