Connection Pooling
Connection pooling is a critical performance optimization feature in the Xec execution engine, particularly for SSH connections where establishing new connections is expensive.
Overview
Connection pooling (packages/core/src/adapters/ssh/index.ts) provides:
- Automatic connection reuse across multiple commands to the same host — entirely internal, there is no manual borrow/release API
- A single pool size limit (
maxConnections) rather than separate min/max - Idle timeout management for resource cleanup
- Keepalive pings to detect a dead connection before it's reused
- Metrics on connections created, destroyed, reused and currently active
SSH Connection Pooling
Basic Usage
import { $ } from '@xec-sh/core';
// Connection pooling is automatic for SSH
const remote = $.ssh({
host: 'server.example.com',
username: 'deploy',
privateKey: '~/.ssh/id_rsa'
});
// These commands reuse the same connection
await remote`uptime`;
await remote`free -h`;
await remote`df -h`;
Pool Configuration
Pool behavior is adapter configuration, not a per-target option — there is
no pool field on $.ssh(options). Configure it when the engine is built,
or by registering a pre-configured adapter:
import { ExecutionEngine, createCallableEngine } from '@xec-sh/core';
const engine = new ExecutionEngine({
adapters: {
ssh: {
connectionPool: {
maxConnections: 20, // default: 10
idleTimeout: 60000, // ms before an idle connection is evicted; default: 300000 (5 min)
keepAlive: true, // default: true
keepAliveInterval: 30000, // ms between keepalive pings; default: 30000
}
}
}
});
const $ = createCallableEngine(engine);
import { $, SSHAdapter } from '@xec-sh/core';
// Or register a configured adapter on the default engine
$.registerAdapter('ssh', new SSHAdapter({
connectionPool: { maxConnections: 20 }
}));
There is no separate min (a floor of pre-created connections) — the pool
only grows on demand, up to maxConnections.
Pool Metrics
import { SSHAdapter } from '@xec-sh/core';
const adapter = $.getAdapter('ssh') as SSHAdapter;
const metrics = adapter.getPoolMetrics();
console.log('Active connections:', metrics.activeConnections);
console.log('Idle connections:', metrics.idleConnections);
console.log('Total ever created:', metrics.connectionsCreated);
console.log('Reused (avoided a new connection):', metrics.reuseCount);
getPoolMetrics() returns activeConnections, idleConnections,
totalConnections, connectionsCreated, connectionsDestroyed,
connectionsFailed, reuseCount, averageIdleTime, averageUseCount and
lastCleanup. There is no .warmPool(), .drainPool() or .closePool()
on the SSH context — connections are created lazily on first use per host,
and released when the engine is disposed ($.dispose() or
await dispose() for the global engine).
Connection Lifecycle Events
There is no connection:state event and no manual connection state
enum — but the engine does emit real events for the SSH connection
lifecycle, on $ itself (not on the target-bound context):
$.on('ssh:connect', ({ host, port, username }) => {
console.log(`Connected to ${host}`);
});
$.on('ssh:disconnect', ({ host, reason }) => {
console.log(`Disconnected from ${host}: ${reason ?? 'unknown reason'}`);
});
$.on('ssh:reconnect', ({ host, attempts, success }) => {
console.log(`Reconnect attempt ${attempts} for ${host}: ${success ? 'ok' : 'failed'}`);
});
$.on('ssh:pool-metrics', ({ metrics }) => {
if (metrics.activeConnections / metrics.totalConnections > 0.8) {
console.warn('Pool utilization high');
}
});
$.on('ssh:pool-cleanup', ({ cleaned, remaining, reason }) => {
console.log(`Evicted ${cleaned} idle connections (${reason}), ${remaining} left`);
});
Health Checks
There is no configurable validation strategy (validateOnBorrow, a custom
validator function, and so on) — keepalive is the health check, and a
connection that fails it is evicted and replaced automatically the next
time it's needed. keepAlive/keepAliveInterval (shown above) are the only
two knobs.
Multi-Host Pooling
Each host gets its own connections within the pool automatically — there is
one pool per adapter instance, keyed by host, not one pool per $.ssh()
call:
const hosts = [
'web1.example.com',
'web2.example.com',
'web3.example.com'
];
// Each of these targets the same underlying pool, keyed by host
const remotes = hosts.map(host => $.ssh({ host, username: 'deploy' }));
await Promise.all(
remotes.map(remote => remote`systemctl restart nginx`)
);
Performance Optimization
Connection Reuse Patterns
// Good: reuse one context for batch operations
const remote = $.ssh({ host: 'server.com', username: 'user' });
async function deployApplication() {
await remote`git pull`;
await remote`npm install`;
await remote`npm run build`;
await remote`pm2 restart app`;
}
// Also fine: pooling matches by host/user/port, so separately-constructed
// contexts targeting the same host still share a connection
async function alsoFine() {
await $.ssh({ host: 'server.com', username: 'user' })`git pull`;
await $.ssh({ host: 'server.com', username: 'user' })`npm install`;
}
Creating a context with $.ssh({...}) is cheap either way — it doesn't
open a connection by itself, only running a command against it does, and
the pool is what decides whether that reuses an existing connection.
Connection Multiplexing
SSHAdapterConfig has a multiplexing field
({ enabled, controlPath?, controlPersist? }, mirroring OpenSSH's
ControlMaster), but it is not currently wired into connection
establishment — setting it has no effect yet. The connection pool described
above is what actually avoids repeated handshakes today.
Error Handling
There is no pool:error event, no built-in retry/circuit-breaker
configuration for the pool, and no recreateOnError option — a failed
connection surfaces as a normal thrown error (typically a ConnectionError
or an ExecutionError with kind: 'connection-lost' /
'connection-refused') from whatever command tried to use it:
import { ExecutionError } from '@xec-sh/core';
try {
await remote`some-command`;
} catch (error) {
if (error instanceof ExecutionError && error.recoverable) {
// connection-lost or connection-refused — a fresh attempt may succeed
}
}
Build retry behavior on top with retry() or $.retry({...}), the same as
for any other command — see Error Handling.
Best Practices
Do's ✅
// ✅ Reuse one context for multiple commands against the same host
const remote = $.ssh({ host: 'server.com', username: 'user' });
await remote`command1`;
await remote`command2`;
// ✅ Size the pool for your actual concurrency
const engine = new ExecutionEngine({
adapters: { ssh: { connectionPool: { maxConnections: 20 } } }
});
// ✅ Dispose the engine when your program is done with it, so pooled
// connections are closed instead of left open
await $.dispose();
// ✅ Watch utilization via the real metrics
$.on('ssh:pool-metrics', ({ metrics }) => {
if (metrics.activeConnections >= metrics.totalConnections) {
console.warn('Pool saturated');
}
});
Don'ts ❌
// ❌ Assume every $.ssh({...}) call opens a new connection — it doesn't;
// the pool matches on host/user/port regardless of how many contexts
// point at them
// ❌ Set maxConnections far above what you'll actually run concurrently
const oversized = new ExecutionEngine({
adapters: { ssh: { connectionPool: { maxConnections: 1000 } } }
});
// ❌ Exit the process without disposing the engine
process.exit(0); // Pooled connections are not closed cleanly
Implementation Details
Connection pooling is implemented in:
packages/core/src/adapters/ssh/index.ts-SSHAdapter's connection pool, keepalive and evictionpackages/core/src/adapters/ssh/connection-pool-metrics.ts-ConnectionPoolMetricsCollector