Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ $ npm install agentkeepalive --save
* `socketActiveTTL` {Number} Sets the socket active time to live, even if it's in use.
If not set, the behaviour keeps the same (the socket will be released only when free)
Default = `null`.
* `testOnBorrow` {Boolean} When set to `true`, validates free sockets using wall-clock
time before reuse. Sockets idle longer than their effective `freeSocketTimeout` are
destroyed and replaced with fresh connections. This is essential for environments where
the process is suspended between invocations (e.g., AWS Lambda, Azure Functions) and
`socket.setTimeout()` callbacks do not fire during the freeze.
Default = `false`.

## Usage

Expand Down Expand Up @@ -178,6 +184,35 @@ const req = http

This behavior is consistent with Node.js core. But through `agentkeepalive`, you can use this feature in older Node.js version.

### Usage with Serverless (AWS Lambda, Azure Functions)

In serverless environments, the process is frozen between invocations and `socket.setTimeout()` callbacks do not fire. Enable `testOnBorrow` to validate sockets using wall-clock time before reuse:

```js
const https = require('https');
const HttpsAgent = require('agentkeepalive').HttpsAgent;

const agent = new HttpsAgent({
keepAlive: true,
freeSocketTimeout: 30000,
testOnBorrow: true, // validate sockets against real elapsed time before reuse
});

// Use in your Lambda handler
exports.handler = async (event) => {
const res = await new Promise((resolve, reject) => {
https.get('https://api.example.com/data', { agent }, (res) => {
res.resume(); // drain the response to free the socket back to the pool
res.on('end', () => resolve(res));
res.on('error', reject);
}).on('error', reject);
});
// ...
};
```

Without `testOnBorrow`, sockets that were idle during a process freeze may appear valid (their timeout timer was frozen too) but the server has already closed the connection — resulting in `EPIPE` or `ECONNRESET` errors.

## [Benchmark](https://github.com/node-modules/agentkeepalive/tree/master/benchmark)

run the benchmark:
Expand Down
3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface Constants {
SOCKET_NAME: Symbol;
SOCKET_REQUEST_COUNT: Symbol;
SOCKET_REQUEST_FINISHED_COUNT: Symbol;
SOCKET_ENTER_FREE_POOL_TIME: Symbol;
SOCKET_ENTER_FREE_POOL_TIMEOUT: Symbol;
}

/**
Expand All @@ -49,6 +51,7 @@ declare namespace AgentKeepAlive {
freeSocketKeepAliveTimeout?: number | undefined;
timeout?: number | undefined;
socketActiveTTL?: number | undefined;
testOnBorrow?: boolean | undefined;
}

export interface HttpOptions extends http.AgentOptions, CommonHttpOption { }
Expand Down
57 changes: 57 additions & 0 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const {
SOCKET_NAME,
SOCKET_REQUEST_COUNT,
SOCKET_REQUEST_FINISHED_COUNT,
SOCKET_ENTER_FREE_POOL_TIME,
SOCKET_ENTER_FREE_POOL_TIMEOUT,
} = require('./constants');

// OriginalAgent come from
Expand Down Expand Up @@ -159,12 +161,67 @@ class Agent extends OriginalAgent {
socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT], customTimeout);
return false;
}

// When testOnBorrow is enabled, stamp the wall-clock time and effective timeout
// so addRequest() can validate sockets using real elapsed time instead of timers.
// This is needed in environments where the process is frozen (e.g., AWS Lambda)
// and socket.setTimeout() callbacks do not fire.
if (this.options.testOnBorrow) {
socket[SOCKET_ENTER_FREE_POOL_TIME] = Date.now();
socket[SOCKET_ENTER_FREE_POOL_TIMEOUT] = customTimeout;
}

if (socket.timeout !== customTimeout) {
socket.setTimeout(customTimeout);
}
return true;
}

/**
* Remove sockets from the free pool that have been idle longer than
* their effective timeout using wall-clock time. This handles environments
* where the process is frozen between invocations (e.g., AWS Lambda) and
* socket.setTimeout() callbacks do not fire.
* @param {string} name - The socket pool key (from getName()).
*/
_purgeStaleFreeSockets(name) {
const freeSockets = this.freeSockets[name];
if (!freeSockets) return;

const now = Date.now();
for (let i = freeSockets.length - 1; i >= 0; i--) {
const socket = freeSockets[i];
const enterTime = socket[SOCKET_ENTER_FREE_POOL_TIME];
const timeout = socket[SOCKET_ENTER_FREE_POOL_TIMEOUT];
if (enterTime && timeout && (now - enterTime) > timeout) {
debug('%s(requests: %s, finished: %s) %dms idle, %dms threshold, destroying stale socket',
socket[SOCKET_NAME], socket[SOCKET_REQUEST_COUNT], socket[SOCKET_REQUEST_FINISHED_COUNT],
now - enterTime, timeout);
freeSockets.splice(i, 1);
this.timeoutSocketCount++;
socket.destroy();
Comment thread
grn621 marked this conversation as resolved.
}
}
if (freeSockets.length === 0) {
delete this.freeSockets[name];
}
}

addRequest(req, options, ...args) {
if (this.options.testOnBorrow) {
// Node's super.addRequest() merges agent options over request options before
// calling getName(). We must replicate that merge here so our pool-key matches
// the one Node uses to store sockets (important for HTTPS options like ca/cert/key).
const opts = typeof options === 'string'
? { host: options, port: args[0], localAddress: args[1] }
: options;
const effectiveOpts = { ...opts, ...this.options };
this._purgeStaleFreeSockets(this.getName(effectiveOpts));
}
// Pass original arguments untouched — let Node handle them as it always did.
super.addRequest(req, options, ...args);
}

// only call on addRequest
reuseSocket(...args) {
// reuseSocket(socket, req)
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ module.exports = {
SOCKET_NAME: Symbol('agentkeepalive#socketName'),
SOCKET_REQUEST_COUNT: Symbol('agentkeepalive#socketRequestCount'),
SOCKET_REQUEST_FINISHED_COUNT: Symbol('agentkeepalive#socketRequestFinishedCount'),
SOCKET_ENTER_FREE_POOL_TIME: Symbol('agentkeepalive#socketEnterFreePoolTime'),
SOCKET_ENTER_FREE_POOL_TIMEOUT: Symbol('agentkeepalive#socketEnterFreePoolTimeout'),
};
Loading