Jump to solution
Verify

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%.

Jump to Verify Open PR/Commit
@@ -429,3 +429,84 @@ Metric Meaning `~ConnectionPool.check()` or by the `!check` callback ======================= ===================================================== + + +.. _pool-sqlalchemy:
repro.py
#! /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()
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md
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.
Production impact:
  • 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

Discussion

High-signal excerpts from the issue thread (symptoms, repros, edge-cases).

“I have tested that the suggested implementation works as expected”
@dvarrazzo · 2025-04-16 · source
“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…”
@dvarrazzo · 2025-04-16 · source
“I am re-opening the issue to provide a better alternative.”
@dvarrazzo · 2025-05-04 · source
“@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…”
@chrispy-snps · 2025-05-04 · source

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
signature.txt
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.txt
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

repro.py
#! /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.

When NOT to use: Do not use if it changes public behavior or if the failure cannot be reproduced.

Fix reference: https://github.com/psycopg/psycopg/pull/1067

Last verified: 2026-02-09. Validate in your environment.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

When NOT to Use This Fix

  • Do not use if it changes public behavior or if the failure cannot be reproduced.

Verify Fix

verify
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.