node-quartz
v1.0.0
Published
Distributed, resilient, Redis‑backed job scheduler for Node.js (cron, multi‑queue, retries + DLQ, definition store, CLI)
Downloads
15
Maintainers
Readme
Node Quartz — Distributed, Resilient, Redis‑Backed Job Scheduler
A modern, fault‑tolerant job scheduler for Node.js with cron support, multi‑queue workers, retries + DLQ, definition stores (memory/file/custom), and Redis‑based coordination.
Features
- Cron scheduling with seconds and optional timezone per job
- Distributed coordination via Redis with jittered master heartbeat
- Multi‑queue workers using
BLMOVE(fallback toRPOPLPUSH) for low‑latency polling - Retries with exponential backoff and a failed (DLQ) queue
- Job Definition Store: load from memory, file, or custom source; synced to Redis across instances
- Powerful CLI: inspect/requeue failed jobs, import/export, manage definitions (add/remove/list/reload)
- Safe processors: run scripts from
scriptsDiror provide in‑memory processors map - Redis v4 async client support and clean shutdown semantics
- TypeScript types, GitHub Actions CI, and Docker Compose integration tests
Why Node Quartz?
- Simplicity and resilience first: minimal moving parts using Redis keyspace notifications for scheduling and straightforward worker semantics.
- Flexible definitions: load jobs from memory/file/custom stores, keep them in sync across instances, and manage them via CLI.
- Cron‑centric: native cron parsing (with seconds) and per‑job timezone support without complex pipelines.
- Multi‑queue without ceremony: just list your queues and go; no heavyweight queue abstraction.
- Pragmatic reliability: jittered master heartbeat to avoid synchronized renewals and clean shutdown guards to prevent noisy errors.
How it compares
- Bull/BullMQ: Great for queue processing with rich features; Node Quartz focuses on cron scheduling + simple job execution with lighter footprint.
- Agenda/Bree: Similar cron scheduling space; Node Quartz adds Redis‑backed definition sync, multi‑queue workers, and a focused CLI.
Architecture
+--------------------------+
| Job Store (opt) |
| - memory / file / custom|
+------------+-------------+
| load() / upsert
v
+------------------+
| Redis |
|------------------|
| defs:index (SET) |
| defs:<id> (STR) |<-- CLI defs:add/remove/reload
| defs:events (PUB)|----^
| |
| jobs (LIST/KEYS) |<-- enqueue/TTL (:next/:retry)
| processing (LIST)|
| failed (LIST) |<-- CLI failed:list/requeue/delete
| master (KEY) | (pubsub: __keyevent__ expired)
+--------+---------+
^
pubsub (events) | keyspace events (expired)
+---------------------+----------------------+
| |
v v
+--+----------------+ +------+---------------+
| Scheduler A | | Scheduler B |
|-------------------| |---------------------|
| - master election |<-- heartbeat ----->| - standby/worker |
| - schedule cron | | - schedule on events|
| - worker loop |<-- BL/MOVE/RPOP -->| - worker loop |
| - processors | | - processors |
+--+----------------+ +------+---------------+
| processors (scriptsDir) | processors (scriptsDir)
v v
[job module fn] [job module fn]
CLI: interacts directly with Redis (defs:*, failed, jobs) to inspect
and control state; changes propagate via defs:events.Installation
It's on NPM.
npm install node-quartzQuick Start
# 1) Start Redis with keyspace notifications enabled
# Local (requires redis-server installed):
redis-server --notify-keyspace-events Ex
# Or via Docker:
docker run --rm -p 6379:6379 redis:7 redis-server --notify-keyspace-events Ex
# 2) In another terminal, run the demo (closes after ~35s)
# Optionally set REDIS_URL if Redis isn't on localhost
export REDIS_URL=redis://127.0.0.1:6379
npm run demo
# 3) Inspect failed jobs (if any) with the CLI
npx quartz failed:list --count 20 --redis "$REDIS_URL" --prefix quartz:test
# Requeue the first failed job and reset attempts
npx quartz failed:requeue --idx 0 --reset --redis "$REDIS_URL" --prefix quartz:testUsage
// create an instance
const create = require('node-quartz');
const quartz = create({
scriptsDir: '/my/scripts/path',
prefix: 'quartz', // optional, defaults to 'quartz'
logger: console, // optional, provide your own logger (debug/info/warn/error)
queues: ['high', 'default', 'low'], // optional, defaults to ['default']
heartbeat: { intervalMs: 2000, jitterMs: 500 }, // optional jittered master heartbeat
// Optional: persist and sync job definitions across instances
// Uncomment one of the store options below
// store: { type: 'memory', jobs: [ /* preloaded Job objects */ ] },
// store: { type: 'file', path: './jobs.json' },
// store: { type: 'custom', impl: myStore },
redis: {
// Prefer url form; legacy host/port also supported
url: process.env.REDIS_URL || 'redis://localhost:6379'
},
// Optional: provide in-memory processors map instead of requiring files
// processors: { 'scriptToRun': async (job) => { /* ... */ } }
});
// schedule a job (6-field cron with seconds)
const job = {
id: 'job_id',
script: 'scriptToRun', // resolved relative to scriptsDir
cron: '*/10 * * * * *', // every 10 seconds
data: { any: 'payload' }, // optional payload passed to your script via job.data
queue: 'high', // optional, defaults to 'default'
options: {
currentDate: null,
endDate: new Date(Date.now() + 60 * 1000),
tz: 'America/New_York' // optional, timezone for cron schedule
}
};
quartz.scheduleJob(job);
// Later, shut down cleanly (unsubscribe and quit Redis):
// await quartz.close();Your job processor module (at /my/scripts/path/scriptToRun.js) should export a function
and may be sync, callback-based, or async (Promise):
// /my/scripts/path/scriptToRun.js
module.exports = function (job, done) {
// access job.data if you provided it
console.log('processing job', job.id, job.data);
// do work, then call done(err?)
done();
};
// or, Promise/async style
// module.exports = async function (job) {
// console.log('processing job', job.id, job.data);
// // await work
// };Requirements
- Node.js >= 14
- Redis with keyspace notifications for
expiredenabled (notify-keyspace-events Ex).- Local example:
redis-server --notify-keyspace-events Ex - Docker Compose/service: add args
--notify-keyspace-events Ex
- Local example:
Redis Options
redis.url: connection string, e.g.redis://localhost:6379redis.database: database index (number). Defaults to 0. Keyspace notifications subscriptions use this DB for__keyevent@<db>__:*.- Legacy
redis.hostandredis.portare still accepted and converted to a URL
Other Options
prefix: Redis key prefix (defaultquartz). Keys:<prefix>:jobs,<prefix>:processing,<prefix>:jobs:<id>,<prefix>:jobs:<id>:next,<prefix>:master.logger: pluggable logger withdebug/info/warn/errormethods; defaults toconsole.processors: object map of{ [scriptName]: processorFn }to avoid dynamic require().queues: array of queue names to poll (default['default']). Jobs withjob.queueare pushed to<prefix>:q:<queue>:jobsand processed atomically into<prefix>:q:<queue>:processing.heartbeat:{ intervalMs?: number, jitterMs?: number }controls master heartbeat frequency and random jitter (defaults: 2000ms interval, ±500ms jitter).store: load and synchronize job definitions across instances.- Memory:
{ type: 'memory', jobs: [/* Job */] } - File:
{ type: 'file', path: './jobs.json' }(JSON array of Job objects) - Custom:
{ type: 'custom', impl }whereimplimplementsload/list/save/remove
- Memory:
Job Definitions Sync (Store)
- Definitions are stored in Redis under:
- Set:
<prefix>:defs:index - Keys:
<prefix>:defs:<jobId>(stringified Job) - PubSub:
<prefix>:defs:events(JSON messages:{action:'upsert'|'remove'|'reload', id?})
- Set:
- On startup: loads from configured store (optional), upserts to Redis, loads all Redis definitions, and schedules them.
- CLI can manage definitions; changes propagate via PubSub to all instances.
Retries and Failures
- Set
retryon the job (top-level or underoptions.retry):maxAttempts: number of retry attemptsbackoff: either a number (base delay ms, exponential) or an object{ delay: number, factor?: number, maxDelay?: number }
- On failure:
- If attempts remain, the job is scheduled for retry using key expiry (
<prefix>:jobs:<id>:retry). - If exhausted, the job is pushed to
<prefix>:failedwith minimal error info.
- If attempts remain, the job is scheduled for retry using key expiry (
- Cron jobs: on success they reschedule to the next run; on failure they follow the retry policy for the current run, and continue with future schedules after retries are exhausted.
Worker Loop
- A background worker loop consumes
<prefix>:jobsviaBRPOPLPUSH, moves items to<prefix>:processing, and runs your processor. - With multiple queues, the worker attempts an atomic
BLMOVEfrom each queue's jobs list to its processing list (Redis >= 6.2), falling back to round‑robinRPOPLPUSHwith short sleeps. - On startup, it recovers orphaned items from each
<prefix>:q:<queue>:processingback to<prefix>:q:<queue>:jobs. close()stops the loop and quits Redis gracefully.
API
- Factory:
const quartz = create(options) - Methods:
scheduleJob(job): schedule or enqueue a job (supports 6-field cron with seconds)getJob(jobId, cb): fetch stored job payloadremoveJob(jobId, cb): delete stored job payloadlistJobsKey(cb): list all persisted job keys for the prefixclose(cb?): stop worker loop and quit Redis connections
- Events (
quartz.eventsis an EventEmitter):scheduled(job, nextDate)started(job)succeeded(job)failed(job, error)retryScheduled(job, delayMs)
The library uses node-redis v4 (async).
CLI
Install globally or use via npx:
List failed jobs:
quartz failed:list --prefix quartz --redis redis://localhost:6379 --count 20Requeue a failed job:
quartz failed:requeue --idx 0 --prefix quartz --redis redis://localhost:6379 --resetDelete a failed job:
quartz failed:delete --idx 0 --prefix quartz --redis redis://localhost:6379Purge failed queue:
quartz failed:purge --prefix quartz --redis redis://localhost:6379Inspect by id:
quartz failed:get --id <jobId> --prefix quartz --redis redis://localhost:6379Requeue by id:
quartz failed:requeue-id --id <jobId> --prefix quartz --redis redis://localhost:6379 --resetDelete by id:
quartz failed:delete-id --id <jobId> --prefix quartz --redis redis://localhost:6379Export failed to file:
quartz failed:drain-to-file --out failed.json --prefix quartz --redis redis://localhost:6379 --purgeImport failed from file:
quartz failed:import-from-file --in failed.json --prefix quartz --redis redis://localhost:6379Requeue from file:
quartz failed:import-from-file --in failed.json --requeue --resetList job definitions:
quartz defs:list --prefix quartz --redis redis://localhost:6379Add a job definition from file:
quartz defs:add --file job.json --prefix quartz --redis redis://localhost:6379Remove a job definition:
quartz defs:remove --id job_id --prefix quartz --redis redis://localhost:6379Ask instances to reload defs:
quartz defs:reload --prefix quartz --redis redis://localhost:6379
You can also set env vars: REDIS_URL and QUARTZ_PREFIX.
Testing
- Start Redis with keyspace notifications:
redis-server --notify-keyspace-events Ex - Run tests:
npm test - CI workflow runs tests against Redis (with notifications enabled) on Node 14/16/18/20.
With Docker Compose
- Run tests in containers (spins Redis and a Node runner):
docker compose up --abort-on-container-exit --exit-code-from test- Or via npm script:
npm run test:compose - The test runner mounts your working directory and uses
REDIS_URL=redis://redis:6379.
