mysql2's pool can only recycle connections when they're idle (idleTimeout + maxIdle).
There's no way to cap a connection's maximum age. Under sustained load a connection never
goes idle, so it lives indefinitely and stays pinned to whichever backend it first connected to.
This is a problem when the pool sits in front of a load-balanced / elastic MySQL topology:
- Aurora / RDS reader endpoints: each pooled connection is routed to one reader for its
lifetime. When a reader is added (auto-scaling) or after a failover, existing
long-lived connections don't move — the new capacity stays underused until connections churn.
(RDS Proxy's reader endpoint doesn't fix this; it also pins a connection to a single reader.)
- Any HAProxy / NLB / proxy setup where you want connections to periodically re-resolve and
rebalance.
A max-lifetime (with jitter) is the standard remedy — e.g. HikariCP maxLifetime, and the
legacy mysqljs/mysql had this via a community fork - https://github.com/vita-mojo/mysql. mysql2 has no equivalent as far as i know.
Describe the solution you'd like
A pool option that discards a connection once it exceeds a maximum age, replacing it on next
borrow, with small random jitter to avoid a synchronized die-off:
mysql.createPool({
// ...
maxLifetime: 600000, // ms; 0 = unlimited (default). Recycle connections older than this.
});
- Name
maxLifetime (ms) to match HikariCP and mysql2's existing ms conventions
(idleTimeout). (The mysqljs/mysql fork called it connectionLifeTime in seconds — either
is fine, but ms + maxLifetime is more consistent here.)
- Apply built-in jitter (e.g. up to 5–10%) so pooled connections don't all expire at once.
Proposed implementation
We've run this behaviour in production for a long time via a fork of the legacy mysqljs/mysql
driver that adds a connectionLifeTime option (in seconds)
(mysqljs/mysql@master...vita-mojo:mysql:master), and we're now
carrying the same logic on mysql2 as a patch-package patch. We'd like to see it upstream, as
mysql2 has no equivalent.
The sketch below is the same ~30-line change, but renamed to the proposed upstream API
— maxLifetime in milliseconds (to match idleTimeout and HikariCP) instead of the fork's
connectionLifeTime in seconds. Naming/units are of course open to whatever maintainers prefer.
Mapped onto mysql2's current internals:
lib/pool_config.js — parse the option:
this.maxLifetime = isNaN(options.maxLifetime) ? 0 : Number(options.maxLifetime);
lib/pool_connection.js — stamp an expiry on creation (with jitter):
if (pool.config.maxLifetime > 0) {
const jitter = Math.floor(Math.random() * (pool.config.maxLifetime / 20 + 1)); // up to 5%
this._expiresAt = Date.now() + pool.config.maxLifetime - jitter;
}
lib/base/pool.js — in getConnection, skip/close expired free connections instead of
handing them out:
while (this._freeConnections.length > 0) {
connection = this._freeConnections.pop();
if (connection._expiresAt && Date.now() > connection._expiresAt) {
connection._pool = null;
spliceConnection(this._allConnections, connection);
connection.destroy(); // frees the slot; a fresh connection is created below
continue;
}
this.emit('acquire', connection);
return process.nextTick(() => { connection._released = false; cb(null, connection); });
}
Plus: register maxLifetime so it isn't flagged by the "invalid configuration option"
validation in lib/connection_config.js. (Optional enhancement: also proactively close expired
connections in the existing _removeIdleTimeoutConnections sweep so they don't linger until the
next borrow.)
Alternatives considered
idleTimeout / maxIdle — idle-based only; busy connections never recycle.
- App-level recycling / scheduled process restarts — works but coarse and app-specific.
- RDS Proxy — helps failover / writer pooling, but its reader endpoint still pins a
connection to a single reader, so it doesn't rebalance reads after scaling.
Prior art
- HikariCP
maxLifetime (de-facto standard for JDBC pools).
mysqljs/mysql community fork adding connectionLifeTime.
I'm happy to open a PR with the implementation above + tests if maintainers are open to it.
mysql2's pool can only recycle connections when they're idle (idleTimeout+maxIdle).There's no way to cap a connection's maximum age. Under sustained load a connection never
goes idle, so it lives indefinitely and stays pinned to whichever backend it first connected to.
This is a problem when the pool sits in front of a load-balanced / elastic MySQL topology:
lifetime. When a reader is added (auto-scaling) or after a failover, existing
long-lived connections don't move — the new capacity stays underused until connections churn.
(RDS Proxy's reader endpoint doesn't fix this; it also pins a connection to a single reader.)
rebalance.
A max-lifetime (with jitter) is the standard remedy — e.g. HikariCP
maxLifetime, and thelegacy
mysqljs/mysqlhad this via a community fork - https://github.com/vita-mojo/mysql.mysql2has no equivalent as far as i know.Describe the solution you'd like
A pool option that discards a connection once it exceeds a maximum age, replacing it on next
borrow, with small random jitter to avoid a synchronized die-off:
maxLifetime(ms) to match HikariCP and mysql2's existing ms conventions(
idleTimeout). (Themysqljs/mysqlfork called itconnectionLifeTimein seconds — eitheris fine, but ms +
maxLifetimeis more consistent here.)Proposed implementation
We've run this behaviour in production for a long time via a fork of the legacy
mysqljs/mysqldriver that adds a
connectionLifeTimeoption (in seconds)(mysqljs/mysql@master...vita-mojo:mysql:master), and we're now
carrying the same logic on
mysql2as apatch-packagepatch. We'd like to see it upstream, asmysql2has no equivalent.The sketch below is the same ~30-line change, but renamed to the proposed upstream API
—
maxLifetimein milliseconds (to matchidleTimeoutand HikariCP) instead of the fork'sconnectionLifeTimein seconds. Naming/units are of course open to whatever maintainers prefer.Mapped onto
mysql2's current internals:lib/pool_config.js— parse the option:lib/pool_connection.js— stamp an expiry on creation (with jitter):lib/base/pool.js— ingetConnection, skip/close expired free connections instead ofhanding them out:
Plus: register
maxLifetimeso it isn't flagged by the "invalid configuration option"validation in
lib/connection_config.js. (Optional enhancement: also proactively close expiredconnections in the existing
_removeIdleTimeoutConnectionssweep so they don't linger until thenext borrow.)
Alternatives considered
idleTimeout/maxIdle— idle-based only; busy connections never recycle.connection to a single reader, so it doesn't rebalance reads after scaling.
Prior art
maxLifetime(de-facto standard for JDBC pools).mysqljs/mysqlcommunity fork addingconnectionLifeTime.I'm happy to open a PR with the implementation above + tests if maintainers are open to it.