The Fix
Enhanced `ConnectionPool` to support integration with SQLAlchemy by adding a `close_returns` parameter, allowing `conn.close()` to return connections to the pool instead of closing them.
Based on closed psycopg/psycopg issue #1046 · PR/commit linked
Production note: Most teams hit this during upgrades or environment changes. Roll out with a canary and smoke critical endpoints (health, OpenAPI/docs) before 100%.
@@ -429,3 +429,84 @@ Metric Meaning
`~ConnectionPool.check()` or by the `!check` callback
======================= =====================================================
+
+
+.. _pool-sqlalchemy:
#! /usr/bin/env python
import logging
import time
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logging.getLogger("psycopg.pool").setLevel(logging.DEBUG)
logging.getLogger("sqlalchemy.pool").setLevel(logging.DEBUG)
import psycopg_pool
import sqlalchemy
db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
# Create a psycopg_pool connection pool
mypool = psycopg_pool.ConnectionPool(
conninfo=db_url.replace("+psycopg", ""),
min_size=0,
max_size=1,
max_idle=5,
)
# Create a SQLAlchemy engine that uses the psycopg_pool connection pool
engine = sqlalchemy.create_engine(
url=db_url,
poolclass=sqlalchemy.pool.NullPool, # disable SQLAlchemy's default connection pool
creator=mypool.getconn, # use psycopg_pool to create connections
)
# Register a 'checkin' event listener to return connections to psycopg_pool
# (https://docs.sqlalchemy.org/en/20/core/events.html#sqlalchemy.events.PoolEvents.checkin)
def return_to_pool(dbapi_connection, connection_record):
mypool.putconn(dbapi_connection)
sqlalchemy.event.listen(engine, "checkin", return_to_pool, named=True)
# Define a function that asks the server for the current time
def do_stuff():
with engine.connect() as connection:
result = connection.execute(sqlalchemy.text("SELECT now()"))
print(f"Current time: {result.fetchone()[0]}")
# Perform two transactions, using the same pool-sourced server connection
print("Doing do_stuff() #1...")
do_stuff()
print("Doing do_stuff() #2...")
do_stuff()
print("Done doing stuff! Waiting 10 seconds...")
# Allow idle pool connection to disconnect from server
time.sleep(10)
# Perform another transaction, requiring a new pool connection to the server
print("Doing do_stuff() #3...")
do_stuff()
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Apply the official fix\nEnhanced `ConnectionPool` to support integration with SQLAlchemy by adding a `close_returns` parameter, allowing `conn.close()` to return connections to the pool instead of closing them.\nWhen NOT to use: Do not use if it changes public behavior or if the failure cannot be reproduced.\n\n
Why This Fix Works in Production
- Trigger: db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
- Mechanism: Enhanced `ConnectionPool` to support integration with SQLAlchemy by adding a `close_returns` parameter, allowing `conn.close()` to return connections to the pool instead of closing them.
- If left unfixed, the same config can fail only in production (env differences), causing startup failures or partial feature outages.
Why This Breaks in Prod
- Production symptom (often without a traceback): db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
Proof / Evidence
- GitHub issue: #1046
- Fix PR: https://github.com/psycopg/psycopg/pull/1067
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.70
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.40
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“I have tested that the suggested implementation works as expected”
“Hello, Connection.close() closes the connection; changing it to become a putconn() is a non-backward compatible change. What you can do is to subclass the connection…”
“I am re-opening the issue to provide a better alternative.”
“@dvarrazzo - I like the idea of enhancing psycogp_pool 3.3.0 to remove the need to subclass psycopg.AsyncConnection. The retroactive support for psycopg < 3.3 would…”
Failure Signature (Search String)
- db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
- `Connection.close()` closes the connection; changing it to become a `putconn()` is a non-backward compatible change.
Copy-friendly signature
Failure Signature
-----------------
db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
`Connection.close()` closes the connection; changing it to become a `putconn()` is a non-backward compatible change.
Error Message
Signature-only (no traceback captured)
Error Message
-------------
db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
`Connection.close()` closes the connection; changing it to become a `putconn()` is a non-backward compatible change.
Minimal Reproduction
#! /usr/bin/env python
import logging
import time
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logging.getLogger("psycopg.pool").setLevel(logging.DEBUG)
logging.getLogger("sqlalchemy.pool").setLevel(logging.DEBUG)
import psycopg_pool
import sqlalchemy
db_url = "postgresql+psycopg://postgres:postgres@hostname:port/database" # CHANGEME
# Create a psycopg_pool connection pool
mypool = psycopg_pool.ConnectionPool(
conninfo=db_url.replace("+psycopg", ""),
min_size=0,
max_size=1,
max_idle=5,
)
# Create a SQLAlchemy engine that uses the psycopg_pool connection pool
engine = sqlalchemy.create_engine(
url=db_url,
poolclass=sqlalchemy.pool.NullPool, # disable SQLAlchemy's default connection pool
creator=mypool.getconn, # use psycopg_pool to create connections
)
# Register a 'checkin' event listener to return connections to psycopg_pool
# (https://docs.sqlalchemy.org/en/20/core/events.html#sqlalchemy.events.PoolEvents.checkin)
def return_to_pool(dbapi_connection, connection_record):
mypool.putconn(dbapi_connection)
sqlalchemy.event.listen(engine, "checkin", return_to_pool, named=True)
# Define a function that asks the server for the current time
def do_stuff():
with engine.connect() as connection:
result = connection.execute(sqlalchemy.text("SELECT now()"))
print(f"Current time: {result.fetchone()[0]}")
# Perform two transactions, using the same pool-sourced server connection
print("Doing do_stuff() #1...")
do_stuff()
print("Doing do_stuff() #2...")
do_stuff()
print("Done doing stuff! Waiting 10 seconds...")
# Allow idle pool connection to disconnect from server
time.sleep(10)
# Perform another transaction, requiring a new pool connection to the server
print("Doing do_stuff() #3...")
do_stuff()
What Broke
Connections are closed instead of being returned to the pool, leading to resource exhaustion.
Fix Options (Details)
Option A — Apply the official fix
Enhanced `ConnectionPool` to support integration with SQLAlchemy by adding a `close_returns` parameter, allowing `conn.close()` to return connections to the pool instead of closing them.
Fix reference: https://github.com/psycopg/psycopg/pull/1067
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- Do not use if it changes public behavior or if the failure cannot be reproduced.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Track RSS + object counts after deployments; alert on monotonic growth and GC pressure.
- Add a long-running test that repeats the failing call path and asserts stable memory.
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.