A robust, configurable object pooling library for Node.js, modeled after Apache Commons Pool 2.
This library helps you manage expensive-to-create or limited resources (database connections, HTTP clients, worker instances, parsers, renderers, etc.) by maintaining a pool of reusable objects with well-defined lifecycle and validation rules.
ObjectFactory<T>: Responsible for creating, destroying, and resetting objects (create,destroy,activate,passivate).PooledObjectValidator<T>: A separate, pluggable component that decides whether an object is still valid. This is the modern, recommended way to handle validation.GenericObjectPool<T>: The main pool implementation. It controls when validation occurs (testOnBorrow,testOnCreate, etc.) and how many validation failures to tolerate viamaxValidationRetries.PooledObject<T>: An internal wrapper that tracks metadata (create time, borrow count, idle time, etc.). Validators receive this wrapper plus context.
Validation is no longer part of ObjectFactory. This separation makes the design cleaner and more testable.
npm install node-poolimport { GenericObjectPool } from 'node-pool';
// 1. Define the factory (creation + lifecycle)
const resourceFactory = {
async create() {
return await createExpensiveResource();
},
async destroy(resource) {
await resource.cleanup();
},
async activate(resource) {
// Reset state before handing to a borrower
},
async passivate(resource) {
// Prepare the resource for return to the pool
}
};
// 2. Define a validator (recommended approach)
const resourceValidator = {
validate(pooledObject, context) {
const resource = pooledObject.getObject();
// You can use pooledObject metadata + context
if (context.reason === 'borrow' && context.attemptNumber > 1) {
console.log(`Retry attempt #${context.attemptNumber}`);
}
return resource.isHealthy();
}
};
// 3. Create the pool
const pool = new GenericObjectPool(resourceFactory, {
maxTotal: 10,
maxIdle: 5,
minIdle: 2,
validator: resourceValidator,
testOnBorrow: true,
testOnCreate: true,
maxValidationRetries: 3, // default is 3
timeBetweenEvictionRuns: 30000,
minEvictableIdleTime: 5 * 60 * 1000
});
// 4. Borrow and return
const resource = await pool.borrowObject();
try {
await resource.doWork();
} finally {
await pool.returnObject(resource);
}
// Pre-populate idle objects
await pool.addObjects(3);
// Inspect
console.log(pool.getStats());
// Shutdown (destroys idle objects)
await pool.close();interface ObjectFactory<T> {
create(): Promise<T> | T;
destroy(obj: T): Promise<void> | void;
activate?(obj: T): Promise<void> | void; // called before borrow
passivate?(obj: T): Promise<void> | void; // called on return
}The factory should focus purely on creation, destruction, and state reset. Do not put health checks here anymore.
Validation is now handled by a dedicated PooledObjectValidator. This is more powerful because validators receive the PooledObject wrapper and a context object.
interface PooledObjectValidator<T> {
validate(
pooledObject: PooledObject<T>,
context: { reason: 'create' | 'borrow' | 'return' | 'idle'; attemptNumber: number }
): boolean | Promise<boolean>;
}- Clear separation of concerns.
- Access to rich metadata (
getBorrowedCount(), idle time, create time, etc.). attemptNumberenables sophisticated retry-aware validation logic.- Easy to swap validation strategies without touching creation code.
const pool = new GenericObjectPool(factory, {
validator: {
validate(pooledObject, context) {
const obj = pooledObject.getObject();
// Example: be more lenient on later attempts
if (context.attemptNumber > 1) {
return obj.isUsableWithWarnings();
}
return obj.isHealthy();
}
},
maxValidationRetries: 3, // how many bad objects we will discard before giving up
testOnBorrow: true,
testOnCreate: true
});Behavior:
- When
testOnBorrowortestOnCreateis enabled, the pool validates newly acquired objects. - If validation fails, the object is destroyed and the pool tries again (up to
maxValidationRetriestimes). - If the pool is below
maxTotal, it will prefer creating a fresh object rather than immediately failing. - After exhausting the retry budget with no valid object,
borrowObject()throwsNoSuchElementExceptionwith a descriptive message.
You can also use the helper:
import { createPooledObjectValidator } from 'node-pool';
const validator = createPooledObjectValidator((obj, context) => {
return obj.isHealthy();
});| Option | Default | Description |
|---|---|---|
maxTotal |
8 | Hard limit on total objects (active + idle) |
maxIdle |
8 | Maximum idle objects to keep |
minIdle |
0 | Minimum idle objects the pool tries to maintain |
maxValidationRetries |
3 | Max number of validation failures tolerated during a single borrow/create before giving up |
validator |
null | PooledObjectValidator instance (recommended) |
testOnCreate |
false | Validate objects immediately after creation |
testOnBorrow |
false | Validate objects before returning them to callers |
testOnReturn |
false | Validate objects when they are returned to the pool |
testWhileIdle |
false | Validate idle objects during background eviction |
maxWait |
-1 | Max time (ms) to wait for an object when exhausted (-1 = wait forever) |
blockWhenExhausted |
true | Whether to block when no objects are available |
lifo |
true | true = return most recently used first |
fairness |
false | Fair (FIFO) waiting for exhausted pools |
timeBetweenEvictionRuns |
-1 | How often (ms) the background evictor runs |
minEvictableIdleTime |
30min | Minimum time an object must be idle before it can be evicted |
softMinEvictableIdleTime |
-1 | Like above, but will not drop below minIdle |
Always use try/finally (or equivalent) to guarantee return:
const obj = await pool.borrowObject();
try {
// use obj
} finally {
await pool.returnObject(obj);
}If an object becomes unusable while you have it, invalidate it instead of returning:
const obj = await pool.borrowObject();
try {
await doWork(obj);
} catch (err) {
if (isFatal(err)) {
await pool.invalidateObject(obj);
return;
}
throw err;
} finally {
// Only return if we didn't invalidate
if (/* still valid */) {
await pool.returnObject(obj);
}
}await pool.preparePool(); // ensure minIdle objects exist
await pool.addObjects(10); // add more idle objects proactivelyconst stats = pool.getStats();
console.log(stats);
// { createdCount, destroyedCount, borrowedCount, returnedCount, numActive, numIdle, ... }- Prefer
PooledObjectValidatorover embedding validation in the factory. - Keep
maxValidationRetriessmall (the default of 3 is usually sufficient). High values can hide factory bugs. - Use
testOnBorrowfor correctness-critical resources. UsetestWhileIdlefor expensive health checks that you don't want on the hot path. - Implement
activate/passivateto make objects safe for reuse without leaking state between borrowers. - Always return or invalidate — never drop references to borrowed objects.
- Set reasonable
minEvictableIdleTimeandtimeBetweenEvictionRunsto avoid thrashing the evictor. - Use
getStats()in production for observability (especiallydestroyedByBorrowValidationCount). - Close the pool on shutdown so idle resources are properly cleaned up.
NoSuchElementException— thrown byborrowObject()when the pool cannot provide a valid object (exhausted, closed, or too many validation failures).IllegalStateException/PoolClosedException— returned objects must have been obtained from this pool and not already returned/invalidated.
import { DefaultEvictionPolicy } from 'node-pool';
const pool = new GenericObjectPool(factory, {
validator,
evictionPolicy: new MyCustomEvictionPolicy(),
timeBetweenEvictionRuns: 10000
});lifo: true(default) — most recently returned objects are handed out first (good for cache locality).lifo: false— oldest objects are handed out first.
Set fairness: true if you want threads waiting for objects to be served in FIFO order when the pool is exhausted.
This library follows the same core ideas:
- Separate factory for lifecycle vs validator for health.
PooledObjectfor metadata.- Configurable validation points + bounded retry on acquisition.
- Idle object eviction with pluggable policy.
It is deliberately general purpose — not tied to any specific resource type.
MIT