npm package: @nxtedition/timers
Description: Optimized timer pooling for Node.js. Delays >= 1000ms are batched into a single 500ms-resolution interval, reducing the number of active handles and improving performance when many long-lived timers are in use.
Latest version: 1.1.1

# @nxtedition/timers

Optimized timer pooling for Node.js. Delays >= 1000ms are batched into a single 500ms-resolution interval, reducing the number of active handles and improving performance when many long-lived timers are in use.

Short delays (< 1000ms) fall through to the native `setTimeout` for full resolution.

## Install

```sh
npm install @nxtedition/timers
```

## Usage

```js
import { setTimeout, clearTimeout } from '@nxtedition/timers'

// Short delays use native setTimeout (full resolution)
const handle = setTimeout(
  (data) => {
    console.log(data)
  },
  50,
  'hello',
)

// Long delays use pooled timers (500ms resolution, lower overhead)
const pooled = setTimeout(
  (data) => {
    console.log(data)
  },
  5000,
  'world',
)

// Clear works for both
clearTimeout(handle)
clearTimeout(pooled)
```

### Opaque data

The third argument is passed directly to the callback, avoiding closure allocations:

```js
setTimeout(
  (ctx) => {
    ctx.connection.write(ctx.payload)
  },
  5000,
  { connection, payload },
)
```

### Refreshing timers

Pooled timers (delay >= 1000ms) support `.refresh()` to reset the delay without creating a new timer:

```js
const handle = setTimeout(
  () => {
    console.log('idle timeout')
  },
  30000,
  undefined,
)

// On activity, reset the timer
handle.refresh()
```

## API

### `setTimeout(callback, delay, opaque)`

Schedule `callback(opaque)` after `delay` milliseconds.

- `delay < 1000` — delegates to native `globalThis.setTimeout`
- `delay >= 1000` — uses the pooled timer mechanism (500ms resolution)

Returns a handle that can be passed to `clearTimeout`.

### `clearTimeout(handle)`

Cancel a pending timer. Works with both native and pooled handles. Passing `null` or `undefined` is a no-op.

## How it works

All pooled timers share a single `setTimeout` handle that fires every 500ms. On each tick, pending timers are checked and expired ones are fired. This means pooled timers have at most ~500ms of jitter but dramatically reduce the number of active OS timer handles when many long-lived timers are in use (e.g. connection idle timeouts, retry delays).

**Note:** Timers with the same delay are not guaranteed to fire in creation order. The execution order of same-delay timers is nondeterministic.

The internal interval is `unref()`'d so it does not keep the process alive.

## Benchmark

```sh
yarn benchmark
```

Results on Apple M3 Pro / Node.js 25.3.0:

| Benchmark                      | native setTimeout | pooled setTimeout | speedup |
| ------------------------------ | ----------------: | ----------------: | ------: |
| create + clear                 |         250.91 ns |          58.90 ns |    4.3x |
| create + clear (GC)            |           1.95 µs |          73.99 ns |   26.4x |
| batch create + clear 100x (GC) |          41.84 µs |           9.80 µs |    4.3x |

Pooled timers avoid native OS timer handles, which are expensive C++ objects for the GC to trace. The advantage grows under GC pressure — up to **26x** faster when GC is forced per iteration.

## License

MIT