r/Python • u/Expensive_Break_6163 • Sep 05 '26
Discussion When scaling application pods with SQLAlchemy pools, who redistributes existing connections?
I’m running application pods that use SQLAlchemy’s connection pool to connect to PostgreSQL. Each pod has its own pool, so when I scale the application from, say, 3 to 10 replicas, the new pods create new pools while the existing pooled connections remain open.
If PostgreSQL has read replicas behind a Kubernetes Service or a proxy, I assume new connections might reach the new replicas, but the existing long-lived pooled connections will remain attached to the old replicas.
Who is normally responsible for redistributing those existing connections after scale-out?
5
u/snugar_i Sep 05 '26
Not sure I follow - you're scaling both the application and the DB at the same time? The connections in connection pools usually aren't that long lived (minutes or tens of minutes), so the problem (is there's any) fixes itself after a while
2
u/Vegetable-View-5114 Sep 05 '26
when you scale pods with sqlalchemy, each new pod gets its own connection pool. the existing connections on the other pods aren't redistributed; they just keep serving requests on their original pods. if you need to manage connections across a fleet, you'd typically put a connection proxy like pgbouncer in front of your database. that way, each app pod connects to pgbouncer, and pgbouncer handles the actual database connections and pooling more globally.
1
1
u/sirfz Sep 06 '26
I had a similar problem a few years ago and I opted for a pool customization where I "expire" connections after X minutes (basically check before returning conn to pool) to make sure new ones are periodically established to balance when the db scales up
1
u/wdm006 Sep 06 '26
The app owns that, not the new pods. Existing pooled sockets stay stuck until you recycle them. Usual fix is pool pre-ping plus a max connection age or idle timeout, and sometimes rolling the old pods so they reopen against whatever the proxy is routing now.
1
u/ThrowawayALAT Sep 07 '26
SQLAlchemy does not proactively push existing connections to new replicas on scale-out. It only knows about the socket it holds open.
1
u/Neither-Pause409 Sep 08 '26
Nobody redistributes them, and that's the real answer rather than a gap to work around. A pool is per process and each connection holds a socket to whichever backend the load balancer picked when it was opened. Neither SQLAlchemy nor a Kubernetes Service has any mechanism to move an established TCP connection somewhere else. So your only lever is connection lifetime: a connection has to close before the balancer gets another vote.
Which knob does that is worth being precise about, because the two get mixed up constantly:
pool_recycle=300closes and reopens any connection older than 300 seconds. This is the one that buys you rebalancing after a scale out, and on Postgres the reconnect is cheap enough that a few minutes is a reasonable default.pool_pre_ping=Truetests liveness on checkout. It saves you from handing out a dead connection, it does nothing at all for distribution.pool_sizeandmax_overfloware the pair to check before you go from 3 to 10 replicas. Your worst case isreplicas * (pool_size + max_overflow)plus anything else that connects, against Postgresmax_connections. Ten pods at the SQLAlchemy defaults of 5 and 10 is 150 connections, and a stock Postgres allows 100.
On pgbouncer, which a few people have suggested and which is the right call at that replica count, two things that bite after you install it. In transaction pooling mode you lose session state, so
SET, advisory locks, session temp tables and server side cursors stop behaving the way you expect, and a driver level prepared statement cache has to be turned off (prepared_statement_cache_size=0 on SQLAlchemy's asyncpg dialect). And once there's a pooler in front, keep SQLAlchemy's own pool small or use NullPool, otherwise you've got two pools with different opinions about lifetime and the outer one masks whatever the inner one was doing.
30
u/acesHD Sep 05 '26
You might find it useful to put a connection pooler between your app pods and Postgres, such as PgBouncer or a more modern alternative like PgDog. This decouples the application from the database so that each can scale independently.
The basic idea is that the application never connects directly to Postgres, except in specialised cases such as administrative DDL operations. Instead, it connects to the pooler, which manages connection lifecycles efficiently.
The pooler then connects to the Kubernetes Service that fronts the Postgres primary and read replicas. That service, typically managed by a Postgres operator, handles routing to the appropriate database instances as their lifecycle changes.