Skip to content

fix(db): apply the configured connection params to the read replica URL - #37691

Merged
yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
devin/lit-5692-bug-db-connection-pool-settings-are-never-applied-to
Aug 21, 2026
Merged

yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
devin/lit-5692-bug-db-connection-pool-settings-are-never-applied-to

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Read replica ignored the configured DB pool cap
  • Reader pool fell back to Prisma's CPU-based default
  • Only writer and direct URLs got connection params

How it solves it:

  • Reader URL now inherits the writer's pool params
  • One allowlist keeps schema-affecting params on the writer
  • Params pinned on the replica URL still win

User Flow

Before: an operator caps DB connections to protect their database, but the read replica opens as many connections as it likes

  1. They set general_settings.database_connection_pool_limit: 3 and database_connection_pool_timeout: 20, point DATABASE_URL_READ_REPLICA at their reader, and start the proxy
  2. They send a few hundred concurrent GET http://localhost:4592/key/list with the master key, all returning 200
  3. They count live connections on the database and see the reader holding 11 of them, Prisma's num_physical_cpus * 2 + 1 default, far more than the 3 they configured
  4. The database hits its own connection ceiling under load even though the cap they configured says it should not

After: the same cap applies to the reader, so the total stays where the operator put it

  1. They set the same two settings, point DATABASE_URL_READ_REPLICA at their reader, and start the proxy
  2. They send the same concurrent GET http://localhost:4592/key/list, all returning 200
  3. They count live connections and see the reader holding exactly 3, matching the configured cap
  4. If they had pinned ?connection_limit=50 on the replica URL themselves, that value is still what gets used
  5. A search_path they set on the writer, whether on DATABASE_URL or through database_extra_connection_params, still does not reach the reader, so replica queries keep resolving against the reader's own schema

Relevant issues

Linear ticket

Resolves LIT-5692

Review notes

Inheritance is an allowlist of pool and timeout params (connection_limit, pool_timeout, connect_timeout, socket_timeout, pgbouncer), deliberately not a denylist. A denylist over Postgres' open-ended parameter space cannot be completed, and the one you forget is the one that hurts: options carries search_path, so copying it wholesale would silently repoint every replica query at the writer's schema. An allowlist fails closed, and a parameter nobody has vetted for the reader stays on the writer by default. There is a test pinning exactly that, so flipping this back to a denylist breaks the build rather than quietly widening what the reader inherits.

Both startup paths share the one allowlist. That matters for the CLI in particular, because database_extra_connection_params is an untyped passthrough whose keys override everything else, so without the filter an operator could route a writer search_path to the reader through config rather than through the URL. The CLI does still set options on the reader, but it builds that value from the reader's own options plus the configured statement and lock timeouts, never from the writer's.

History note: this branch carries a staging merge, 0bf8fe09ba, that was authored by someone other than the PR author, resolving conflicts against the Azure Entra ID token-auth work. The resolution was reviewed independently before being adopted: the db_url_settings.py import block is the union of both sides rather than a pick, which matters because taking either side alone yields a module that imports cleanly and fails at runtime, and the reader-parameter allowlist, its two helpers, the CLI call site, and all four regression tests are present and unchanged.

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 (5/5 at d3f801f) before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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

Shared setup: a real local Postgres, a replica URL pointing at the same database, and pg_stat_activity.application_name used purely to tell the two pools apart. The configured cap is 3 throughout. A namespaced port is deliberate: --port 4000 silently rebinds to a random port when it is busy, so on a shared machine the health check can be answered by an unrelated proxy

docker run -d --name lit5692-pg -p 6592:5432 -e POSTGRES_PASSWORD=litpw -e POSTGRES_DB=litellm postgres:16
docker exec lit5692-pg psql -U postgres -d litellm -q -c "create schema if not exists writer_schema;"

cat > /tmp/lit5692.yaml <<'EOF'
model_list: []
general_settings:
  master_key: sk-1234
  database_connection_pool_limit: 3
  database_connection_pool_timeout: 20
EOF

export LITELLM_MASTER_KEY=sk-1234 DISABLE_SCHEMA_UPDATE=true
python litellm/proxy/proxy_cli.py --config /tmp/lit5692.yaml --port 4592 &

count_conns() {
  seq 3000 | xargs -P 100 -I{} curl -s -o /dev/null -H "Authorization: Bearer sk-1234" http://localhost:4592/key/list &
  for _ in 1 2 3 4 5 6; do
    sleep 4
    docker exec lit5692-pg psql -U postgres -d litellm -At -F' | ' \
      -c "select coalesce(nullif(application_name,''),'(unset)'), count(*) from pg_stat_activity where datname='litellm' group by 1 order by 1;" | tr '\n' ' '
    echo
  done
}

Case 1 pins the reader's own marker, so the reader pool is directly countable

export DATABASE_URL="postgresql://postgres:litpw@localhost:6592/litellm"
export DATABASE_URL_READ_REPLICA="postgresql://postgres:litpw@localhost:6592/litellm?options=-c%20application_name%3Dlitellm_reader"

Case 2 pins a search_path and a marker on the writer URL and nothing on the reader, so writer options reaching the reader would relabel the reader's connections too

export DATABASE_URL="postgresql://postgres:litpw@localhost:6592/litellm?options=-c%20search_path%3Dwriter_schema%20-c%20application_name%3Dlitellm_writer"
export DATABASE_URL_READ_REPLICA="postgresql://postgres:litpw@localhost:6592/litellm"

Case 3 does the same through config instead of the URL, which is the passthrough that overrides everything else

export DATABASE_URL="postgresql://postgres:litpw@localhost:6592/litellm"
export DATABASE_URL_READ_REPLICA="postgresql://postgres:litpw@localhost:6592/litellm"
# add to general_settings in /tmp/lit5692.yaml:
#   database_extra_connection_params:
#     options: "-c search_path=writer_schema -c application_name=litellm_writer"

Before (a974464)

Case 1: reader pool size against a configured cap of 3

  1. Start the proxy with the Case 1 URLs, confirm curl -o /dev/null -w '%{http_code}' http://localhost:4592/health/liveliness returns 200
  2. Run count_conns
  3. The reader holds 11 connections, Prisma's own default, ignoring the configured cap of 3
litellm_reader | 11 psql | 1 (unset) | 1
litellm_reader | 11 psql | 1 (unset) | 1
litellm_reader | 11 psql | 1 (unset) | 1
litellm_reader | 11 psql | 1 (unset) | 1
litellm_reader | 11 psql | 1 (unset) | 1
litellm_reader | 11 psql | 1 (unset) | 1

Case 2: writer URL search_path must not follow the reader

  1. Start the proxy with the Case 2 URLs, confirm liveliness returns 200
  2. Run count_conns
  3. The reader connections stay (unset), so the writer's options never reached them, but they climb to 9 rather than the configured 3
litellm_writer | 1 psql | 1 (unset) | 5
litellm_writer | 1 psql | 1 (unset) | 5
litellm_writer | 1 psql | 1 (unset) | 5
litellm_writer | 1 psql | 1 (unset) | 6
litellm_writer | 1 psql | 1 (unset) | 9
litellm_writer | 1 psql | 1 (unset) | 9

Case 3: configured extra connection params must not carry a search_path to the reader

  1. Start the proxy with the Case 3 config, confirm liveliness returns 200
  2. Run count_conns
  3. The writer picks up the marker, the reader stays (unset), and the reader holds 13 connections rather than the configured 3
litellm_writer | 1 psql | 1 (unset) | 13
litellm_writer | 1 psql | 1 (unset) | 13
litellm_writer | 1 psql | 1 (unset) | 13
litellm_writer | 1 psql | 1 (unset) | 13
litellm_writer | 1 psql | 1 (unset) | 13
litellm_writer | 1 psql | 1 (unset) | 13

After (0bf8fe0)

Case 1: reader pool size against a configured cap of 3

  1. Start the proxy with the Case 1 URLs, confirm liveliness returns 200
  2. Run count_conns
  3. The reader holds exactly 3 connections, the configured cap, and keeps its own litellm_reader marker, so the reader's own options survived the params being appended
litellm_reader | 3 psql | 1 (unset) | 1
litellm_reader | 3 psql | 1 (unset) | 1
litellm_reader | 3 psql | 1 (unset) | 1
litellm_reader | 3 psql | 1 (unset) | 1
litellm_reader | 3 psql | 1 (unset) | 1
litellm_reader | 3 psql | 1 (unset) | 1

Case 2: writer URL search_path must not follow the reader

  1. Start the proxy with the Case 2 URLs, confirm liveliness returns 200
  2. Run count_conns
  3. The reader now holds exactly 3 connections and its connections are still (unset), so it picked up the cap without picking up the writer's search_path
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3

Case 3: configured extra connection params must not carry a search_path to the reader

  1. Start the proxy with the Case 3 config, confirm liveliness returns 200
  2. Run count_conns
  3. The writer still picks up the marker, proving the setting is live, while the reader is capped at 3 and stays (unset)
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3
litellm_writer | 1 psql | 1 (unset) | 3

Type

🐛 Bug Fix

Caveats (if any)

  • Inheritance is an allowlist; unknown params stay on the writer
  • schema and postgres options are never inherited
  • Replica URLs with nothing to add are left byte for byte unchanged

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/2d1f0c3cd9234051a365e54ab1d647d0
Requested by: @yassin-berriai

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

CLAassistant commented Aug 20, 2026

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.
0 out of 2 committers have signed the CLA.

❌ yassin-berriai
❌ yassinkortam
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR now applies configured pool and timeout parameters to read-replica URLs while preventing writer-specific schema options from reaching the replica.

  • Introduces a shared allowlist for replica-safe connection parameters.
  • Preserves parameters explicitly pinned on the replica URL.
  • Filters database_extra_connection_params through the same allowlist in the CLI startup path.
  • Adds regression coverage for writer URL options, configured schema overrides, replica-pinned values, and absent replica URLs.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains from the previously reported writer-options or configured-extra-options replica leaks.

Important Files Changed

Filename Overview
litellm/proxy/db/db_url_settings.py Adds allowlisted connection-parameter inheritance while excluding writer options and schema; the previously reported writer-options leak is fixed.
litellm/proxy/proxy_cli.py Applies only replica-safe configured parameters and derives timeout options from the replica URL itself; the previously reported extra-options leak is fixed.
tests/test_litellm/proxy/db/test_db_url_settings.py Adds focused coverage for inheritance, replica overrides, URL preservation, and exclusion of schema-affecting writer parameters.
tests/test_litellm/proxy/test_proxy_cli.py Adds CLI-level regression coverage confirming configured writer schema options remain absent from the replica URL.

Reviews (3): Last reviewed commit: "fix(db): apply the configured connection..." | Re-trigger Greptile

Comment thread litellm/proxy/db/db_url_settings.py Outdated
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing devin/lit-5692-bug-db-connection-pool-settings-are-never-applied-to (0bf8fe0) with litellm_internal_staging (e07a712)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (33bafd0) during the generation of this report, so e07a712 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@yassin-berriai
yassin-berriai force-pushed the devin/lit-5692-bug-db-connection-pool-settings-are-never-applied-to branch from 0771b61 to 7f888c9 Compare August 20, 2026 21:38
@yassin-berriai

Copy link
Copy Markdown
Contributor

@greptileai re-review 7f888c9: options is out of the inherited allowlist, plus tests pinning allowlist semantics and the reader's own search_path

Comment thread litellm/proxy/proxy_cli.py Outdated
The read replica never received the operator's DB pool settings, so its
Prisma pool fell back to `num_physical_cpus * 2 + 1` and the configured cap
was not enforced. Both startup paths now pass the same params to the reader:
the CLI, and the componentized entrypoints that go through
`DatabaseURLSettings.apply_to_env`.

Only pool and timeout params are inherited, through a single allowlist both
paths share. Anything that decides which tables a query resolves against
stays on the writer, including entries smuggled in through
`database_extra_connection_params`, so a writer `search_path` cannot repoint
reader queries. Params the operator pinned on the replica URL still win.
@yassin-berriai
yassin-berriai force-pushed the devin/lit-5692-bug-db-connection-pool-settings-are-never-applied-to branch from 7f888c9 to d3f801f Compare August 20, 2026 22:13
@yassin-berriai

Copy link
Copy Markdown
Contributor

@greptileai re-review d3f801f: the CLI reader path now filters database_extra_connection_params through the same allowlist, with a regression test covering it

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@yassin-berriai

Copy link
Copy Markdown
Contributor

Staging merge 0bf8fe0 was authored by someone else. Verified independently: import block is the union, allowlist and all four regression tests intact.

@yassin-berriai
yassin-berriai merged commit 43995bc into litellm_internal_staging Aug 21, 2026
71 checks passed
@yassin-berriai
yassin-berriai deleted the devin/lit-5692-bug-db-connection-pool-settings-are-never-applied-to branch August 21, 2026 00:23
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.

4 participants