qjobs
qjobs is an in-memory callback queue for Node.js. You add functions plus an argument object or array, choose a maximum concurrency, attach EventEmitter listeners, and call run. Each function must call next to release its slot. The queue can pause launches, abort pending work, report counters, accept work while already running, and insert a delay between full batches. It does not persist jobs, retry failures, return promises, coordinate multiple processes, or provide workers of its own.
qjobs can remain in stable callback-era code, but its missing error channel and call-next discipline make it a poor new dependency. Use p-queue for promises or fastq for a focused in-memory worker queue.
Use it if
- You are maintaining callback-era Node code that already uses qjobs events and next callbacks
- All work lives in one process and losing pending jobs on restart is acceptable
- You need a small concurrency cap plus pause, abort, progress counters, and lifecycle events
- You want to add jobs while a run is active and can enforce a strict call-next-once convention
- You write promise or async-function code: qjobs ignores returned promises and only frees a concurrency slot when its next callback is invoked, while p-queue and fastq support current async styles
- Failures are part of normal control flow: open issue #7 requests a missing job-error event, and the source neither catches thrown errors nor accepts next(error), so every job must contain its own error handling
- You need reliable reuse after completion: open issue #6 reports that newly added jobs do not start after previous work has ended; add only during an active run or call run again yourself
- Jobs must survive crashes, deploys, or multiple Node processes: jobsList and every counter exist only in one JavaScript object, with no database, acknowledgment, lease, or worker coordination
- You require active maintenance and current typing or module conventions: version 1.2.0 was pushed in February 2018, the package has no TypeScript declarations or ESM entry, and all three current issues remain open
Setup reality
npm install qjobs is the whole install. Version 1.2.0 has no runtime dependencies, peers, native compilation, configuration file, or external service, and its engine declaration permits Node 0.9 and newer. That age is also the warning: the only entry is CommonJS, the API is callback-based, and there are no TypeScript declarations. Construct with new QJobs({ maxConcurrency, interval }), add each job as queue.add(fn, args), register listeners before starting, and call queue.run(). args is not optional in practice. The run method writes an internal _jobId property onto it before calling the job, so passing undefined throws and passing a frozen object fails; your own object or array is mutated. Every job must call next exactly once. Forgetting it permanently occupies a slot, calling it twice corrupts running and done counters, throwing escapes from a setTimeout callback, and returning or rejecting a promise has no effect on queue progress. pause(true) prevents new launches but does not interrupt running jobs, and it starts a one-second timer that emits pause repeatedly until pause(false). abort() only flips a flag; pending entries are discarded on a later run cycle while jobs already executing continue. interval is not a per-job rate limit. It waits between batches after the concurrency ceiling has been filled and running work drains, emitting the misspelled continu event when it resumes. The source's boundary logic is particularly risky with unusual concurrency values, so do not treat it as an API quota scheduler. stats() exposes underscore-prefixed counters and may report NaN progress before any jobs are added. Finally, dynamic additions while work is active are supported, but adding after end does not automatically restart the queue. Call run again and expect another start event because jobsDone can also affect that event logic.
Patterns
Create a concurrency-limited queuecreate-queue
const QJobs = require('qjobs');
const queue = new QJobs({ maxConcurrency: 4 });The default concurrency is 10 when maxConcurrency is omitted or otherwise falsy.
Add a callback jobadd-callback-job
queue.add(function fetchOne(args, next) {
fetchRecord(args.id, function (error, record) {
args.error = error || null;
args.record = record;
next();
});
}, { id: 42 });qjobs mutates the args value by adding _jobId, and the job must call next exactly once even on failure.
Attach listeners and start workstart-queue
queue.on('start', () => console.log('queue started'));
queue.on('end', () => console.log('queue drained'));
queue.run();Register listeners before run because start is emitted synchronously at the beginning of the call.
Observe individual job transitionsobserve-jobs
queue.on('jobStart', (args) => {
console.log('starting', args._jobId, args.id);
});
queue.on('jobEnd', (args) => {
console.log('finished', args._jobId, args.id);
});The internal numeric _jobId is written onto the same args object or array supplied to add.
Adapt a promise task safelywrap-promise-job
queue.add(function runAsync(args, next) {
Promise.resolve()
.then(() => processItem(args.item))
.then(
(value) => { args.value = value; next(); },
(error) => { args.error = error; next(); }
);
}, { item });Returning the promise is not enough; qjobs does not observe it, so both settlement branches must call next.
Pause and resume pending launchespause-launches
queue.pause(true);
setTimeout(() => {
queue.pause(false);
}, 5000);Pausing does not cancel jobs already running, and the queue emits pause once per second until resumed.
Track how long the queue is pausedreport-pause-time
queue.on('pause', (milliseconds) => {
console.log('paused for', milliseconds, 'ms');
});
queue.on('unpause', () => console.log('resumed'));pause is a repeated timer event, not a one-time state-change event; unpause fires once when the timer is cleared.
Poll queue statisticsread-progress
const timer = setInterval(() => {
const stats = queue.stats();
console.log(stats._status, stats._progress + '%', stats._jobsRunning);
if (stats._status === 'Finished') clearInterval(timer);
}, 1000);The returned field names begin with underscores, and progress can be NaN when the queue has no jobs.
Change the launch limitchange-concurrency
queue.setConcurrency(8);Changing the value does not preempt running jobs or validate the number; use a positive integer and test interval behavior.
Delay between concurrency batchesdelay-between-batches
const queue = new QJobs({
maxConcurrency: 5,
interval: 1000,
});
queue.on('sleep', () => console.log('batch drained, waiting'));
queue.on('continu', () => console.log('starting next batch'));The event is spelled continu in version 1.2.0, and interval is batch spacing rather than a strict per-time-window rate limit.
Discard work that has not startedabort-pending-jobs
queue.abort();abort sets a flag and clears pending jobs on a later run cycle; already running jobs continue and still must call next.
Run jobs added after the queue endedrestart-after-end
queue.on('end', () => {
// Later, when more work arrives:
});
function enqueueLater(item) {
queue.add(worker, { item });
queue.run();
}A completed queue does not automatically notice later additions, as tracked in open issue #6; explicitly call run again.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| p-queue | npm | Choose it for modern promise tasks, concurrency and interval caps, priorities, timeouts, and idle promises |
| fastq | npm | Choose it for a fast in-memory worker queue with callback and promise interfaces plus drain and error hooks |
| async | npm | Choose it when concurrency-limited queues are one of several callback and async control-flow tools you need |