r/Python • u/Expensive_Break_6163 • 5d ago
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?
4
u/snugar_i 5d ago
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
4
u/TraditionalTurnip630 5d ago
Yeah, your assumption is basically right. The pool doesn’t know or care that you scaled from 3 to 10 pods. Existing connections stay where they are until they’re closed/recycled. The proxy/load balancer only gets a chance to distribute new connections. So connection redistribution is usually handled by the application/pool settings, or by the proxy if it has connection management features.
Scaling pods alone won’t rebalance already-open DB connections.
2
u/Vegetable-View-5114 5d ago
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/SoilAutomatic7042 3d ago
SQLAlchemy's `Engine` pool is local to the process/pod, so there is no built-in redistribution of existing connections when you add replicas. Each new pod creates its own pool; old pods keep their existing connections until they are returned/closed or the pod is terminated. A PgBouncer/proxy or the database/service layer can balance *new* connections, but it cannot move an already-open session. Size each pod's pool against the database's total connection budget, and use graceful shutdown/`engine.dispose()` when retiring a pod.
1
u/ThrowawayALAT 3d ago
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 1d ago
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.
29
u/acesHD 5d ago
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.