[Django] #37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks `**pool_options` on top, raising `TypeError`.

23 views
Skip to first unread message

Django

unread,
Apr 28, 2026, 12:51:44 PMApr 28
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Type: Bug
Castro Cambiaghi | Component: Database
Status: new | layer (models, ORM)
Version: 5.2 | Severity: Normal
Keywords: postgresql psycopg | Triage Stage:
pool | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
== Reproduction ==

`django/db/backends/postgresql/base.py` (5.2.13) constructs the pool like
this — see
[https://github.com/django/django/blob/5.2.13/django/db/backends/postgresql/base.py#L209-L215
base.py L209-L215]:

{{{
#!python
enable_checks = self.settings_dict["CONN_HEALTH_CHECKS"]
pool = ConnectionPool(
kwargs=connect_kwargs,
open=False,
configure=self._configure_connection,
check=ConnectionPool.check_connection if enable_checks else None,
**pool_options,
)
}}}

If a user puts a `"check"` key in `OPTIONS["pool"]` to inject a custom
validation callable — which is the documented way to extend
`psycopg_pool.ConnectionPool`'s liveness probe — the call collides at
first cursor open:

{{{
TypeError: psycopg_pool.pool.ConnectionPool() got multiple values for
keyword argument 'check'
}}}

`CONN_HEALTH_CHECKS` does not gate this. With `True`, Django passes
`check=ConnectionPool.check_connection`; with `False`, Django passes
`check=None`. Either way the keyword is set, so any `check` in
`pool_options` collides.

Minimal `settings.py`:

{{{
#!python
def my_check(conn):
conn.execute("SELECT 1").fetchone()

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "...",
"USER": "...",
"PASSWORD": "...",
"HOST": "...",
"PORT": "5432",
"CONN_HEALTH_CHECKS": False, # also fails with True
"OPTIONS": {
"pool": {
"min_size": 4,
"max_size": 20,
"check": my_check,
},
},
}
}
}}}

== Expected behaviour ==

A `"check"` callable provided in `OPTIONS["pool"]` should win over
Django's default. The documented contract for `OPTIONS["pool"]` is that
the dict is forwarded to `psycopg_pool.ConnectionPool`, and `check` is a
public, documented `ConnectionPool.__init__` parameter — see
[https://www.psycopg.org/psycopg3/docs/api/pool.html#psycopg_pool.ConnectionPool
psycopg pool docs].

== Use case / motivation ==

The driving use case is '''AWS Aurora !PostgreSQL writer-flip handling'''.
After a planned or unplanned writer failover, existing TCP connections
survive but are now bound to a host that has become read-only. The default
liveness probe (`SELECT 1`) does not detect this — the connection is
"alive" but any subsequent `INSERT`/`UPDATE` fails with `cannot execute X
in a read-only transaction`. AWS's
[https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.BestPractices.FastFailover.html
Fast failover with Aurora PostgreSQL guide] recommends chaining a
`pg_is_in_recovery()` check into pool validation — exactly what
`psycopg_pool`'s `check` callable parameter exists for. Django 5.x makes
this impossible without subclassing `DatabaseWrapper` and overriding
`pool`.

== Proposed fix ==

Use `setdefault` so `OPTIONS["pool"]["check"]`, when present, takes
precedence:

{{{
#!python
enable_checks = self.settings_dict["CONN_HEALTH_CHECKS"]
pool_options.setdefault(
"check", ConnectionPool.check_connection if enable_checks else None
)
pool = ConnectionPool(
kwargs=connect_kwargs,
open=False,
configure=self._configure_connection,
**pool_options,
)
}}}

This is fully backwards-compatible: users not setting
`OPTIONS["pool"]["check"]` get exactly today's behaviour. Users who do set
it get their callable. No new public surface — `OPTIONS["pool"]` is
already documented as forwarding to `ConnectionPool`.

The same shape would make future custom callables (`configure`, `reset`)
overridable too, though that's out of scope here — `configure` in
particular needs more thought because Django chains its own connection
configuration.

== Versions affected ==

* Django 5.0 onward (where pool support landed)
* Confirmed on Django 5.2.13 with psycopg 3.3.3 / psycopg-pool 3.3.0

Happy to put up the patch + a regression test (assert that
`OPTIONS["pool"]["check"]` is the actual callable used, with both
`CONN_HEALTH_CHECKS` values) if the maintainers agree this is the right
shape.
--
Ticket URL: <https://code.djangoproject.com/ticket/37075>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Apr 29, 2026, 3:05:17 PMApr 29
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Owner: Raoni
Castro Cambiaghi | Timo de Castro Cambiaghi
Type: | Status: assigned
Cleanup/optimization |
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Normal | Resolution:
Keywords: postgresql psycopg | Triage Stage: Accepted
pool |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* owner: (none) => Raoni Timo de Castro Cambiaghi
* stage: Unreviewed => Accepted
* status: new => assigned
* type: Bug => Cleanup/optimization

Comment:

Makes sense. Thanks for offering a PR. (We probably want to unpack the
provided kwargs into a copy before mutating.)

I guess we have the same situation with `configure`, but we don't have to
address that now.
--
Ticket URL: <https://code.djangoproject.com/ticket/37075#comment:1>

Django

unread,
Apr 30, 2026, 9:29:15 AMApr 30
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Owner: Raoni
Castro Cambiaghi | Timo de Castro Cambiaghi
Type: | Status: assigned
Cleanup/optimization |
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Normal | Resolution:
Keywords: postgresql psycopg | Triage Stage: Accepted
pool |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Raoni Timo de Castro Cambiaghi):

* has_patch: 0 => 1

Comment:

PR: https://github.com/django/django/pull/21198
--
Ticket URL: <https://code.djangoproject.com/ticket/37075#comment:2>

Django

unread,
Apr 30, 2026, 10:17:20 AMApr 30
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Owner: Raoni
Castro Cambiaghi | Timo de Castro Cambiaghi
Type: | Status: assigned
Cleanup/optimization |
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Normal | Resolution:
Keywords: postgresql psycopg | Triage Stage: Ready for
pool | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* stage: Accepted => Ready for checkin

Comment:

Looks good -- only just suggest removing the release note.
--
Ticket URL: <https://code.djangoproject.com/ticket/37075#comment:3>

Django

unread,
Apr 30, 2026, 7:52:21 PMApr 30
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Owner: Raoni
Castro Cambiaghi | Timo de Castro Cambiaghi
Type: | Status: closed
Cleanup/optimization |
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Normal | Resolution: fixed
Keywords: postgresql psycopg | Triage Stage: Ready for
pool | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls <jacobtylerwalls@…>):

* resolution: => fixed
* status: assigned => closed

Comment:

In [changeset:"9f790ef1a0f356cf6342b5d57bbaeac35aed0d9f" 9f790ef]:
{{{#!CommitTicketReference repository=""
revision="9f790ef1a0f356cf6342b5d57bbaeac35aed0d9f"
Fixed #37075 -- Allowed overriding the PostgreSQL pool's "check" callable.

Setting "check" in OPTIONS["pool"] previously raised TypeError because the
PostgreSQL backend always passed check= to ConnectionPool() and unpacked
**pool_options on top, regardless of CONN_HEALTH_CHECKS. The user's
callable
now takes precedence via setdefault(); pool_options is copied first to
avoid
mutating the user's settings dict.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/37075#comment:4>

Django

unread,
May 6, 2026, 2:37:07 PMMay 6
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Owner: Raoni
Castro Cambiaghi | Timo de Castro Cambiaghi
Type: | Status: closed
Cleanup/optimization |
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Normal | Resolution: fixed
Keywords: postgresql psycopg | Triage Stage: Ready for
pool | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Raoni Timo de Castro Cambiaghi):

Replying to [comment:4 Jacob Walls <jacobtylerwalls@…>]:

Any chance this is backported to 5.2.x? Thanks!
--
Ticket URL: <https://code.djangoproject.com/ticket/37075#comment:5>

Django

unread,
May 6, 2026, 2:41:08 PMMay 6
to django-...@googlegroups.com
#37075: `OPTIONS["pool"]["check"]` cannot be set: the PostgreSQL backend always
passes `check=` to `psycopg_pool.ConnectionPool`, then unpacks
`**pool_options` on top, raising `TypeError`.
-------------------------------------+-------------------------------------
Reporter: Raoni Timo de | Owner: Raoni
Castro Cambiaghi | Timo de Castro Cambiaghi
Type: | Status: closed
Cleanup/optimization |
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Normal | Resolution: fixed
Keywords: postgresql psycopg | Triage Stage: Ready for
pool | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jacob Walls):

No, we don't backport new features.
--
Ticket URL: <https://code.djangoproject.com/ticket/37075#comment:6>
Reply all
Reply to author
Forward
0 new messages