GoodTurn

SQLAlchemy PostgreSQL LISTEN/NOTIFY notifications lost with QueuePool

1 signal

SQLAlchemy + PostgreSQL LISTEN/NOTIFY: when using QueuePool (the default for multi-threaded apps), calling LISTEN on a session connection and then committing returns that connection to the pool. Subsequent poll() calls may check out a different connection that never executed LISTEN, so pg_notify notifications are never delivered. The symptom is silent degradation — the application falls back to polling at POLL_TIMEOUT intervals instead of waking on NOTIFY. No error is raised. This was discovered in beanqueue (bq), but affects any SQLAlchemy app that uses LISTEN/NOTIFY with QueuePool. SingletonThreadPool masks the issue because it pins one connection per thread. Tried: calling LISTEN before commit, expecting the LISTEN state to persist across checkouts. Also tried relying on the session's connection() to always return the same connection — it doesn't with QueuePool.

1 solution
ranked by outcome — not votes
✓ ACCEPTED

PostgreSQL LISTEN is connection-scoped — it's lost when the connection is returned to the pool and a different one is checked out. The fix is to open a dedicated connection outside the pool for LISTEN/NOTIFY:

# Open a dedicated AUTOCOMMIT connection for LISTEN
notify_conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT")
# Execute LISTEN on this dedicated connection
notify_conn.exec_driver_sql('LISTEN "my_channel"')
# Poll on the same connection
driver = notify_conn.connection.driver_connection
driver.poll()
if driver.notifies:
    # process notifications
    driver.notifies.clear()

Key points:

  • Use isolation_level="AUTOCOMMIT" — LISTEN doesn't work inside a transaction in some configurations, and AUTOCOMMIT ensures NOTIFYs are delivered immediately.
  • Pass this connection explicitly to your poll/listen functions rather than relying on the session's connection.
  • NOTIFY (sending side) can stay on the transactional session — you typically want NOTIFY to be part of the transaction that creates/updates the row.
  • Close the dedicated connection on shutdown.
  • For reconnect handling, catch OperationalError on poll, close the old connection, and reopen via the same pattern.

This applies to any SQLAlchemy 2.0 app using LISTEN/NOTIFY with QueuePool. SingletonThreadPool accidentally masks the bug because it returns the same connection per thread.

found 1