A high-performance, zero-dependency resource pooling library for Node.js and TypeScript. Achieve 40M+ operations/sec with intelligent auto-scaling, resource validation, and built-in timeout protection.
Perfect for managing database connections, worker threads, HTTP clients, or any reusable resource with automatic lifecycle management and production-ready reliability.
- Features
- Installation
- Quick Start
- Pool Types
- API Reference
- Validation Rules
- Examples
- Benchmarks
- TypeScript Support
- License
- ⚡ Blazing Fast: 40M+ ops/sec with zero-allocation hot paths
- 🎯 Two Pool Types: Auto-scaling ObjectPool & lightweight EnginePool (index-based)
- 🔄 Flexible Acquisition: Sync (
acquire()), async (acquireAsync()), or automatic (use()) - 🛡️ Production Ready: Resource validation, timeout protection, automatic cleanup
- 🏗️ Static & Dynamic: Fixed-size (min === max) or auto-scaling (min < max) pools
- 📊 Built-In Metrics: Monitor pool utilization, pending requests, and scaling events
- 🔒 Type-Safe: Full TypeScript support with comprehensive type inference
- 🚀 Zero Dependencies: Pure TypeScript, no native bindings, minimal overhead
npm install @lojhan/resource-poolOr with yarn/pnpm:
yarn add @lojhan/resource-pool
pnpm add @lojhan/resource-poolimport { createPool } from '@lojhan/resource-pool';
// Create a pool that auto-scales from 2 to 10 resources
const pool = createPool({
min: 2,
max: 10,
resourceFactory: async () => {
const conn = new DatabaseConnection();
await conn.connect();
return conn;
},
resourceDestroyer: async (conn) => await conn.close(),
validateResource: async (conn) => conn.isConnected(),
});
// Automatic resource management (recommended)
const result = await pool.use(async (connection) => {
return await connection.query('SELECT * FROM users');
});
// Or manual acquire/release
const conn = await pool.acquireAsync(5000); // 5s timeout
try {
await conn.query('SELECT 1');
} finally {
pool.release(conn);
}
// Cleanup
await pool.destroy();The main pool implementation that manages resource lifecycle. Supports both fixed-size and auto-scaling configurations.
Pre-allocates all resources upfront. Best for stable, predictable workloads.
const pool = createPool({
min: 10,
max: 10,
resourceFactory: () => new DatabaseConnection(),
});Best for:
- Known capacity requirements
- Stable workloads
- Minimal latency (all resources pre-created)
Starts with minimum resources, scales up on demand, scales down when idle.
const pool = createPool({
min: 2,
max: 50,
resourceFactory: async () => new DatabaseConnection(),
idleTimeoutMs: 30000, // Remove idle resources after 30s
scaleDownIntervalMs: 10000, // Check every 10s
});Best for:
- Variable workloads
- Traffic spikes
- Resource-constrained environments
- Automatic cleanup
Lightweight pool that manages slot indices instead of resources. For maximum performance and custom resource management.
import { EnginePool } from '@lojhan/resource-pool';
const workers = [new Worker('./worker.js'), new Worker('./worker.js')];
const pool = new EnginePool(workers.length);
const idx = await pool.acquireAsync();
try {
await workers[idx].process(data);
} finally {
pool.release(idx);
}Best for:
- Maximum throughput (39M+ ops/sec)
- Pre-indexed resource arrays
- Load shedding patterns
- Custom resource routing
Creates an ObjectPool for managing resource lifecycle.
function createPool<T extends object>(
config: {
min?: number;
max?: number;
resourceFactory: (() => T) | (() => Promise<T>);
resourceDestroyer?: (resource: T) => void | Promise<void>;
validateResource?: (resource: T) => boolean | Promise<boolean>;
// Timeout protection
factoryTimeoutMs?: number; // Default: 5000
destroyerTimeoutMs?: number; // Default: 5000
validatorTimeoutMs?: number; // Default: 3000
// Error handling
bubbleFactoryErrors?: boolean; // Default: false
bubbleDestroyerErrors?: boolean; // Default: false
bubbleValidationErrors?: boolean; // Default: false
// Auto-scaling (dynamic pools only)
idleTimeoutMs?: number; // Default: 30000
scaleDownIntervalMs?: number; // Default: 10000
// Acquisition
acquireTimeoutMs?: number; // Default: 0 (no timeout)
},
initialResources?: T[],
): IObjectPool<T>;Function that creates new resources. Can be sync or async.
// Sync factory
resourceFactory: () => new Connection();
// Async factory
resourceFactory: async () => {
const conn = new Connection();
await conn.connect();
return conn;
};Pool size boundaries. See Validation Rules for requirements.
// Fixed-size pool (static)
{ min: 10, max: 10 }
// Dynamic pool (auto-scaling)
{ min: 2, max: 50 }
// Static pool from initialResources
{ resourceFactory, /* no min/max */ }Called when resources are destroyed (scale-down, validation failure, or pool destruction).
resourceDestroyer: async (conn) => {
await conn.close();
console.log('Connection closed');
};Validates resources before returning from acquireAsync(). Invalid resources are destroyed and replaced.
validateResource: async (conn) => {
try {
await conn.ping();
return true; // Valid
} catch {
return false; // Will be replaced
}
};Maximum time to wait for resource creation. Prevents hanging on slow factories.
Maximum time to wait for resource destruction. Prevents hanging on cleanup.
Maximum time to wait for resource validation. Treats timeout as invalid.
{
factoryTimeoutMs: 10000, // 10s to create
destroyerTimeoutMs: 5000, // 5s to destroy
validatorTimeoutMs: 2000, // 2s to validate
}Controls whether factory errors in background scale-up operations are thrown or logged.
If true, errors during resource destruction are thrown. If false, errors are silently ignored.
If true, validation errors are thrown. If false, errors are treated as invalid (return false).
{
bubbleDestroyerErrors: true, // Throw on cleanup errors
bubbleValidationErrors: true, // Throw on validation errors
}Duration before idle resources are destroyed. Only applies when min < max.
How often to check for idle resources. Only applies when min < max.
{
min: 5,
max: 50,
idleTimeoutMs: 60000, // Remove after 60s idle
scaleDownIntervalMs: 15000, // Check every 15s
}Default timeout for acquireAsync() if not specified per-call. 0 means no timeout.
{
acquireTimeoutMs: 5000, // Default 5s timeout for all acquires
}Synchronously acquire a resource. Returns null if none available.
const resource = pool.acquire();
if (resource) {
// Use resource
pool.release(resource);
} else {
// Pool exhausted
}Asynchronously acquire resource, waiting if necessary. Throws on timeout.
const resource = await pool.acquireAsync(5000); // 5s timeout
try {
await resource.doWork();
} finally {
pool.release(resource);
}Recommended. Automatically acquires, executes function, and releases resource (even on error).
const result = await pool.use(async (conn) => {
return await conn.query('SELECT * FROM users');
});
// Connection released automaticallyReturn resource to pool.
pool.release(resource);Shutdown pool and destroy all resources.
await pool.destroy();Get current pool statistics.
const metrics = pool.getMetrics();
console.log({
size: metrics.size, // Current active resources
available: metrics.available, // Idle resources
busy: metrics.busy, // In-use resources
capacity: metrics.capacity, // Max capacity
pendingCreates: metrics.pendingCreates, // Resources being created
});Index-based pool for maximum performance.
import { EnginePool } from '@lojhan/resource-pool';
const pool = new EnginePool(size: number);
// Same methods as ObjectPool but returns indices
const idx: number = await pool.acquireAsync();
pool.release(idx);
await pool.use(async (idx) => { ... });createPool() enforces strict validation rules:
resourceFactoryis always required- If
minis specified,maxis required - If
maxis specified,minis required
- When neither
minnormaxare provided:initialResourcesare required- Pool size is
initialResources.length - Pool is fixed-size (min === max)
// ✅ Valid static pool
createPool(
{
resourceFactory: () => new Connection(),
},
[conn1, conn2, conn3],
); // min: 3, max: 3minmust be non-negative (>= 0)maxmust be at least 1 (>= 1)maxmust be >= minmaxcannot exceed INT32_MAX (2,147,483,647)
- If
initialResourcesare provided, length must exactly equal min
// ❌ Error: Static pool requires exactly 5 resources
createPool(
{
min: 5,
max: 5,
resourceFactory: () => new Connection(),
},
[conn1, conn2, conn3],
); // Only 3 provided
// ✅ Valid
createPool(
{
min: 5,
max: 5,
resourceFactory: () => new Connection(),
},
[conn1, conn2, conn3, conn4, conn5],
); // Exactly 5- Cannot exceed
maxcapacity
// ❌ Error: 10 resources exceed max of 5
createPool(
{
min: 2,
max: 5,
resourceFactory: () => new Connection(),
},
tenConnections,
); // 10 resources
// ✅ Valid
createPool(
{
min: 2,
max: 5,
resourceFactory: () => new Connection(),
},
[conn1, conn2, conn3],
); // 3 resources OKmin: 0is allowed for lazy/on-demand poolsinitialResourcesare optional
// ✅ Valid: Lazy pool
createPool({
min: 0,
max: 10,
resourceFactory: () => new Connection(),
}); // Starts with 0 resources, scales up on demandimport { createPool } from '@lojhan/resource-pool';
import { Client } from 'pg';
const pool = createPool({
min: 5,
max: 20,
resourceFactory: async () => {
const client = new Client({
host: 'localhost',
database: 'mydb',
});
await client.connect();
return client;
},
resourceDestroyer: async (client) => {
await client.end();
},
validateResource: async (client) => {
try {
await client.query('SELECT 1');
return true;
} catch {
return false;
}
},
idleTimeoutMs: 60000,
validatorTimeoutMs: 2000,
});
// Use in your application
async function getUser(id: number) {
return pool.use(async (client) => {
const result = await client.query('SELECT * FROM users WHERE id = $1', [id]);
return result.rows[0];
});
}
// Cleanup on shutdown
process.on('SIGINT', async () => {
await pool.destroy();
process.exit(0);
});import { createPool } from '@lojhan/resource-pool';
import { Worker } from 'worker_threads';
const pool = createPool({
min: 4,
max: 8,
resourceFactory: () => new Worker('./worker.js'),
resourceDestroyer: async (worker) => {
await worker.terminate();
},
factoryTimeoutMs: 10000,
});
async function processTask(data: any) {
return pool.use(async (worker) => {
return new Promise((resolve, reject) => {
worker.once('message', resolve);
worker.once('error', reject);
worker.postMessage(data);
});
});
}import { createPool } from '@lojhan/resource-pool';
import fetch from 'node-fetch';
interface HTTPClient {
fetch: typeof fetch;
lastUsed: number;
}
const pool = createPool({
min: 2,
max: 10,
resourceFactory: () => ({
fetch,
lastUsed: Date.now(),
}),
validateResource: (client) => {
// Invalidate clients older than 5 minutes
return Date.now() - client.lastUsed < 5 * 60 * 1000;
},
idleTimeoutMs: 120000,
});
async function makeRequest(url: string) {
return pool.use(async (client) => {
client.lastUsed = Date.now();
const response = await client.fetch(url);
return response.json();
});
}import { EnginePool } from '@lojhan/resource-pool';
import { Worker } from 'worker_threads';
const workers = Array.from({ length: 4 }, () => new Worker('./worker.js'));
const pool = new EnginePool(workers.length);
async function processWithLoadShedding(task: any) {
// Fast-fail if no workers available
const idx = pool.acquire();
if (idx === null) {
throw new Error('SERVICE_OVERLOADED');
}
try {
return await new Promise((resolve, reject) => {
workers[idx].once('message', resolve);
workers[idx].once('error', reject);
workers[idx].postMessage(task);
});
} finally {
pool.release(idx);
}
}
// Health check
function getHealth() {
const metrics = pool.getMetrics();
return {
utilization: (metrics.busy / metrics.capacity) * 100,
available: metrics.available,
};
}import { createPool } from '@lojhan/resource-pool';
// Pre-create expensive resources
const connections = await Promise.all(
Array.from({ length: 5 }, async () => {
const conn = new ExpensiveConnection();
await conn.initialize();
return conn;
}),
);
// Create static pool from existing resources
const pool = createPool(
{
resourceFactory: () => new ExpensiveConnection(), // Fallback (not called if static)
resourceDestroyer: async (conn) => await conn.close(),
},
connections, // Exactly 5 resources, pool is min: 5, max: 5
);
// All resources are immediately available
const conn = pool.acquire(); // Never null in static pool
if (conn) {
// Use connection
pool.release(conn);
}Performance on modern hardware (Apple M1 Pro):
| Library | acquire/release | .use() pattern | vs generic-pool |
|---|---|---|---|
| ObjectPool (Dynamic) | 48.1M ops/sec | 11.7M ops/sec | 25x faster |
| ObjectPool (Static) | 41.6M ops/sec | 12.7M ops/sec | 22x faster |
| EnginePool | 39.2M ops/sec | 13.1M ops/sec | 21x faster |
| generic-pool | 1.9M ops/sec | 1.7M ops/sec | baseline |
| tarn | 0.9M ops/sec | 0.9M ops/sec | 0.5x |
Dynamic vs Static: Dynamic pools (min < max) allow auto-scaling, while static pools (min === max) have fixed size.
Comparison summary:
- generic-pool: ~1.9M ops/sec
- tarn: ~0.9M ops/sec
- @lojhan/resource-pool: 40-48M ops/sec
Run benchmarks locally:
cd benchmarks
npm install
npm run benchFull TypeScript support with comprehensive type inference.
import { createPool, type IObjectPool, type PoolMetrics } from '@lojhan/resource-pool';
interface DatabaseConnection {
query(sql: string): Promise<any>;
close(): Promise<void>;
}
// Type-safe pool
const pool: IObjectPool<DatabaseConnection> = createPool({
min: 5,
max: 10,
resourceFactory: async (): Promise<DatabaseConnection> => {
const conn = new DatabaseConnection();
await conn.connect();
return conn;
},
});
// Type inference works automatically
const result = await pool.use(async (conn) => {
// conn is typed as DatabaseConnection
return await conn.query('SELECT 1');
});
// Metrics are typed
const metrics: PoolMetrics = pool.getMetrics();
console.log(metrics.size, metrics.available, metrics.busy);// ❌ BAD: Prone to leaks if error occurs
const resource = await pool.acquireAsync();
await doSomething(resource);
pool.release(resource);
// ✅ GOOD: Guaranteed release
await pool.use(async (resource) => {
await doSomething(resource);
});const pool = createPool({
min: 5,
max: 10,
resourceFactory: async () => createConnection(),
validateResource: async (conn) => {
try {
await conn.ping();
return true;
} catch {
return false; // Will be replaced
}
},
validatorTimeoutMs: 2000,
});const pool = createPool({
min: 5,
max: 10,
resourceFactory: async () => createConnection(),
factoryTimeoutMs: 10000, // 10s to create
destroyerTimeoutMs: 5000, // 5s to destroy
validatorTimeoutMs: 2000, // 2s to validate
acquireTimeoutMs: 5000, // 5s default acquire timeout
});setInterval(() => {
const metrics = pool.getMetrics();
console.log({
utilization: (metrics.busy / metrics.capacity) * 100,
available: metrics.available,
pending: metrics.pendingCreates,
});
}, 10000);async function shutdown() {
console.log('Shutting down pool...');
await pool.destroy();
console.log('Pool destroyed');
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);Symptoms: acquireAsync() times out frequently
Solutions:
- Increase
maxpool size - Check for resource leaks (not releasing resources)
- Reduce
acquireTimeoutMsto fail faster - Implement load shedding with EnginePool
Symptoms: Pool stays at max size even when idle
Solutions:
- Check
idleTimeoutMsis set (default: 30000) - Verify
min < max(only dynamic pools scale) - Check
scaleDownIntervalMs(default: 10000)
Symptoms: Frequent resource replacements
Solutions:
- Check
validateResourcelogic is correct - Increase
validatorTimeoutMsif validation is slow - Monitor metrics for
pendingCreatesspikes
Symptoms: Memory usage grows over time
Solutions:
- Ensure
resourceDestroyerproperly cleans up - Always use
pool.use()or try/finally with manual acquire - Call
pool.destroy()on shutdown
Contributions welcome! See CONTRIBUTING.md
npm install
npm test
npm run benchMIT © Lojhan