Summary
Every new TLS connection runs a full handshake: the server sends its certificate chain, the client verifies it against the CA list and derives fresh keys. A pool refilling after idle timeouts or scaling up, a pool cluster, or an application opening short-lived connections repeats that against the same server over and over. TLS session resumption lets a client that already completed a handshake present a ticket on later connections and skip most of the work. Node's own HTTPS agent does this by default (maxCachedSessions, 100 per host and port); the driver does not.
With the secure-context cache from the third perf round in place, a new plain TLS connection still costs about 1.9 ms of client CPU, most of it the handshake and chain verification, and the chain is several kilobytes on the wire per connection.
What it saves
- TLS 1.2 (MySQL 5.7 and older builds): the abbreviated handshake needs one round trip instead of two, and no certificate exchange or verification. Over a 10 ms link that is 10 ms less per new connection.
- TLS 1.3 (MySQL 8.0.16+, MariaDB): the round-trip count stays at one, but the certificate chain is neither re-sent nor re-verified. Forward secrecy is kept because the default resumption mode (
psk_dhe_ke) still runs an ephemeral key exchange.
Verified locally before starting (Docker servers)
A throwaway probe (below) wrapped tls.connect so the second and third mysql2 connections presented the session captured from the first, then checked both sides:
| Server |
Protocol |
Client isSessionReused() |
Server side |
| MySQL 8.3.0 |
TLS 1.3 |
true on connections 2 and 3 |
Ssl_session_cache_hits 2 → 4, Ssl_sessions_reused 1, ssl_session_cache_mode=ON, ssl_session_cache_timeout=300 |
| MySQL 9.7.2 |
TLS 1.3 |
true |
same counters and settings |
| MariaDB 12.3.2 |
TLS 1.3 |
true |
counters not exposed (stay 0), client confirms reuse |
| MySQL 5.7.44 |
TLS 1.2 DHE-RSA-AES128-GCM-SHA256 |
true |
Ssl_session_cache_hits 1 → 2, Ssl_sessions_reused 1 |
Two details that matter for the implementation: TLS 1.3 servers send two tickets per connection, and they arrive right after the handshake, before the first query result, so a pool's second connection can already reuse the first one's session.
Proposed implementation
In lib/base/connection.js, next to the secure-context cache:
- Keep the latest ticket per ssl config object + host + port, taken from the socket's
'session' event (it can fire more than once; keep the newest).
- Pass it as the
session option of tls.connect in startTLS. A rejected or expired ticket is not an error: the server silently performs a full handshake, and the next 'session' event replaces the stale ticket.
- Memory only, never persisted; drop the entry when a handshake fails.
Security requirements:
- Node skips
checkServerIdentity on a resumed session because the identity was verified when the ticket was issued, so the cache key must include everything that shaped the original verification: the ssl object (CA, cert, key, ciphers, versions, and its identity as the secure-context cache already relies on), host, port, rejectUnauthorized and verifyIdentity. A session established under a lax config must never be resumed by a strict one.
rejectUnauthorized stays on by default and is not touched by this change.
Tests (MYSQL_USE_TLS=1):
- the second connection of a pool reports
socket.isSessionReused() === true (and on MySQL, SHOW SESSION STATUS LIKE 'Ssl_sessions_reused' is 1);
- a connection with different ssl material or a different
rejectUnauthorized does not reuse;
- a failed handshake drops the cached session;
- the existing TLS suite still passes on MySQL 5.7 (TLS 1.2), 8.x, 9 and MariaDB.
Expected gain: roughly half to one millisecond of client CPU and a few kilobytes per new TLS connection, plus one round trip per connection on TLS 1.2 servers.
Probe script
Run from the repository root against a TLS-enabled server (MYSQL_PORT=3308 node probe.js); it monkey-patches tls.connect for the probe only, the driver is unchanged.
'use strict';
const tls = require('node:tls');
const mysql = require('./promise.js');
const PORT = Number(process.env.MYSQL_PORT || 3306);
const originalConnect = tls.connect;
let capturedSession = null;
const sockets = [];
tls.connect = function (options, callback) {
if (capturedSession) {
options = { ...options, session: capturedSession };
}
const socket = originalConnect.call(tls, options, callback);
socket.on('session', (session) => {
capturedSession = session;
});
sockets.push(socket);
return socket;
};
async function connectAndReport(label) {
const conn = await mysql.createConnection({
host: '127.0.0.1',
port: PORT,
user: 'root',
database: 'test',
ssl: { rejectUnauthorized: false },
});
const socket = sockets[sockets.length - 1];
const [status] = await conn.query(
"SHOW SESSION STATUS WHERE Variable_name IN ('Ssl_version','Ssl_sessions_reused','Ssl_session_cache_hits')"
);
console.log(
label,
socket.getProtocol(),
socket.getCipher().name,
'isSessionReused =',
socket.isSessionReused(),
Object.fromEntries(status.map((r) => [r.Variable_name, r.Value]))
);
await new Promise((r) => setTimeout(r, 300));
await conn.end();
}
(async () => {
await connectAndReport('connection 1');
await connectAndReport('connection 2 (presenting captured session)');
await connectAndReport('connection 3 (presenting captured session)');
})().catch((e) => {
console.error(e);
process.exit(1);
});
Summary
Every new TLS connection runs a full handshake: the server sends its certificate chain, the client verifies it against the CA list and derives fresh keys. A pool refilling after idle timeouts or scaling up, a pool cluster, or an application opening short-lived connections repeats that against the same server over and over. TLS session resumption lets a client that already completed a handshake present a ticket on later connections and skip most of the work. Node's own HTTPS agent does this by default (
maxCachedSessions, 100 per host and port); the driver does not.With the secure-context cache from the third perf round in place, a new plain TLS connection still costs about 1.9 ms of client CPU, most of it the handshake and chain verification, and the chain is several kilobytes on the wire per connection.
What it saves
psk_dhe_ke) still runs an ephemeral key exchange.Verified locally before starting (Docker servers)
A throwaway probe (below) wrapped
tls.connectso the second and third mysql2 connections presented the session captured from the first, then checked both sides:isSessionReused()trueon connections 2 and 3Ssl_session_cache_hits2 → 4,Ssl_sessions_reused1,ssl_session_cache_mode=ON,ssl_session_cache_timeout=300truetrueDHE-RSA-AES128-GCM-SHA256trueSsl_session_cache_hits1 → 2,Ssl_sessions_reused1Two details that matter for the implementation: TLS 1.3 servers send two tickets per connection, and they arrive right after the handshake, before the first query result, so a pool's second connection can already reuse the first one's session.
Proposed implementation
In
lib/base/connection.js, next to the secure-context cache:'session'event (it can fire more than once; keep the newest).sessionoption oftls.connectinstartTLS. A rejected or expired ticket is not an error: the server silently performs a full handshake, and the next'session'event replaces the stale ticket.Security requirements:
checkServerIdentityon a resumed session because the identity was verified when the ticket was issued, so the cache key must include everything that shaped the original verification: the ssl object (CA, cert, key, ciphers, versions, and its identity as the secure-context cache already relies on), host, port,rejectUnauthorizedandverifyIdentity. A session established under a lax config must never be resumed by a strict one.rejectUnauthorizedstays on by default and is not touched by this change.Tests (
MYSQL_USE_TLS=1):socket.isSessionReused() === true(and on MySQL,SHOW SESSION STATUS LIKE 'Ssl_sessions_reused'is1);rejectUnauthorizeddoes not reuse;Expected gain: roughly half to one millisecond of client CPU and a few kilobytes per new TLS connection, plus one round trip per connection on TLS 1.2 servers.
Probe script
Run from the repository root against a TLS-enabled server (
MYSQL_PORT=3308 node probe.js); it monkey-patchestls.connectfor the probe only, the driver is unchanged.