Debounce and throttle helpers are some of the most commonly copied snippets in the Node.js world, usually pulled in from lodash or a small npm package. Node.js now ships both in core as util.debounce() and util.throttle(), together with crypto.parsePKCS12() for reading .p12 and .pfx certificate bundles. The APIs arrived in Node.js 26.10.0, and InfoQ’s report on 14 November 2026 notes that Node.js 26 is now Active LTS (“Lithium”), so these helpers are available on the long-term-support line. This tutorial shows how to use all three with practical examples and explains the options that differ from the lodash versions you may know.
Which versions have them
The Node.js 26.10.0 release notes list:
util: implement debounce(#65899)util: implement util.throttle(#65899)crypto: add crypto.parsePKCS12()(#65627)
The util documentation marks both helpers as “Added in: v26.10.0”. Check your runtime:
node -v # needs v26.10.0 or newer
node -p "typeof require('node:util').debounce" # 'function'
If you support older Node.js lines, keep your existing dependency there.
util.debounce(fn, wait[, options])
A debounced function waits until calls stop for wait milliseconds, then runs fn once with the arguments from the most recent call.
One big difference from lodash: the debounced function returns a Promise for fn‘s return value. By default, every call made during one debounce window resolves with the result of the single invocation.
import { debounce } from 'node:util';
import { writeFile } from 'node:fs/promises';
let settings = {};
const saveSettings = debounce(async () => {
await writeFile('settings.json', JSON.stringify(settings, null, 2));
return Date.now();
}, 500);
// Called many times in quick succession, written to disk once
for (const theme of ['dark', 'light', 'dark']) {
settings.theme = theme;
saveSettings();
}
const savedAt = await saveSettings();
console.log('saved at', savedAt);
Options
| Option | Default | What it does |
|---|---|---|
leading |
false |
Run fn immediately when a new window starts |
rejectOnCancel |
false |
Calls replaced by a later call reject with an AbortError instead of sharing the result |
signal |
none | An AbortSignal that cancels pending calls and blocks future ones |
With leading: true, the first call runs at once. A trailing call happens only if the function was called again during the window.
Controls on the returned function
The debounced function has extra properties:
cancel([reason])cancels the current window. Pending promises reject with anAbortError.flush()runsfnimmediately if a call is pending.pendingis the promise from the latest call in the window, ornull.pendingCountis the number of calls waiting.ref()andunref()control whether the timer keeps the event loop alive.
flush() is very handy for graceful shutdown:
process.on('SIGTERM', async () => {
saveSettings.flush(); // write any pending change now
await saveSettings.pending; // wait for it to finish
process.exit(0);
});
And unref() stops a pending debounce from keeping a CLI tool running:
const log = debounce(sendTelemetry, 2000).unref();
util.throttle(fn, limit, interval[, options])
Throttle limits how often fn runs: at most limit calls per interval milliseconds. Unlike many userland versions, Node’s throttle queues extra calls by default instead of dropping them, and returns a Promise for each call’s result. That makes it a simple rate limiter for outgoing API requests.
import { throttle } from 'node:util';
// At most 5 requests per second to a partner API
const fetchUser = throttle(async (id) => {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}, 5, 1000);
const ids = Array.from({ length: 20 }, (_, i) => i + 1);
const users = await Promise.all(ids.map((id) => fetchUser(id)));
console.log(users.length); // 20, started in batches of 5 per second
Options
| Option | Default | What it does |
|---|---|---|
concurrency |
Infinity |
Max invocations whose results are still unsettled |
maxPending |
Infinity |
Max queued calls when overflow is 'queue' |
overflow |
'queue' |
'queue' waits; 'drop' rejects immediately |
signal |
none | Cancels pending calls and blocks future ones |
strict |
false |
Guarantees limit is never exceeded in any rolling interval |
Two details from the docs are worth knowing:
- Rate capacity is used when
fnstarts, not when a call joins the queue. Concurrency capacity is released when the returned value settles. - By default the window starts with the first call, so calls can bunch up at window boundaries. Set
strict: truewhen an upstream API counts requests over a rolling window.
Shedding load instead of queueing
For a webhook receiver you might prefer to reject extra work quickly:
const handle = throttle(processEvent, 50, 1000, {
overflow: 'drop',
concurrency: 10,
});
app.post('/webhook', async (req, res) => {
try {
await handle(req.body);
res.sendStatus(202);
} catch (err) {
if (err.code === 'ERR_THROTTLED') return res.sendStatus(429);
throw err;
}
});
Rejected calls get an ERR_THROTTLED error. The rejected promise is marked as handled, so ignoring it won’t trigger an unhandledRejection warning. Rejected calls don’t use up capacity or join the queue.
Cancelling with AbortSignal
Both helpers accept a signal. That ties neatly into request lifecycles and shutdown:
const controller = new AbortController();
const limited = throttle(job, 2, 1000, { signal: controller.signal });
// later, for example on shutdown
controller.abort(new Error('shutting down'));
After abort, pending and future calls reject with an AbortError, with your reason set as the error’s cause.
crypto.parsePKCS12(bundle[, options])
PKCS#12 files (.p12 or .pfx) bundle a private key with its certificate chain. They are common for client-certificate authentication with banks, payment gateways and government APIs. Node.js could already use them through the TLS pfx option, but it couldn’t give you the key and certificates as JavaScript objects. Developers often shelled out to the openssl CLI or used node-forge to do that.
From the crypto docs:
import { parsePKCS12 } from 'node:crypto';
import { readFileSync } from 'node:fs';
const { privateKey, certificate, additionalCertificates } = parsePKCS12(
readFileSync('client.p12'),
{ passphrase: process.env.P12_PASSPHRASE },
);
console.log(privateKey?.asymmetricKeyType); // e.g. 'rsa'
console.log(certificate?.subject);
console.log(certificate?.validTo);
console.log(additionalCertificates.length, 'chain certificates');
What you get back:
privateKey: aKeyObjectfor the first private key, ornull.certificate: anX509Certificatematching the key, ornull.additionalCertificates: every other certificate (all certificates if there is no key).
If you omit passphrase, it’s treated as an empty string.
A practical use: certificate expiry checks
const daysLeft = Math.floor(
(new Date(certificate.validTo) - Date.now()) / 86_400_000
);
if (daysLeft < 30) {
console.warn(`Client certificate expires in ${daysLeft} days`);
}
You can also export the key to PEM for libraries that expect text:
const pem = privateKey.export({ type: 'pkcs8', format: 'pem' });
Keep exported keys out of logs and source control.
Should you drop lodash.debounce?
On Node.js 26.10+ for server code, the core helpers remove a dependency and add Promise results, AbortSignal support and queueing. The behaviour isn’t identical to lodash, though: results come back as Promises, throttle queues by default, and the option names differ. Test before swapping them in. Browser code still needs a userland version.
Key takeaways
util.debounce()andutil.throttle()are in core from Node.js 26.10.0 and are available on the Node.js 26 LTS line.- Both return Promises, accept an
AbortSignaland expose controls such asflush(),cancel()andunref(). throttlequeues by default. Useoverflow: 'drop'andERR_THROTTLEDfor load shedding.crypto.parsePKCS12()reads.p12and.pfxbundles intoKeyObjectandX509Certificateobjects without OpenSSL CLI calls.
Sources: Node.js 26.10.0 release notes, Node.js util docs, Node.js crypto docs and InfoQ.
