A lightweight, PostgreSQL-backed background job queue for Node.js with durable storage, retries, delayed execution, worker leasing, crash recovery, and Dead Letter Queue (DLQ) support.
Jobs are persisted in PostgreSQL, allowing them to survive worker crashes and application restarts.
- Durable PostgreSQL-backed job storage
- Background workers
- Configurable worker concurrency
- Delayed jobs
- Scheduled execution
- Priority-based scheduling
- Automatic retries with exponential backoff
- Dead Letter Queue (DLQ)
- Replay failed jobs
- Lease-based job processing (visibility timeout)
- Automatic lease renewal (heartbeats)
- Recovery of abandoned jobs
- Graceful worker shutdown
- Queue statistics
- Job execution history
This package has not yet been published to npm.
The current implementation relies on an internal Prisma schema and generated Prisma Client. Before the first npm release, the installation experience will be simplified so users can initialize the queue with minimal setup.
Until then, the project can be used directly from the source repository.
Clone the repository.
git clone https://github.com/<your-username>/reliable-job-queue.git
cd reliable-job-queueInstall dependencies.
npm install- Node.js 20+
- PostgreSQL
Create a .env file in the project root.
DATABASE_URL="postgresql://postgres:password@localhost:5432/reliable_job_queue"Generate the Prisma Client.
npm run prisma:generateApply the database migrations.
npm run prisma:migrateor
npx prisma migrate devBuild the project.
npm run buildThe queue is now ready to use.
import { Queue } from "@reliable-job-queue/core";
const queue = new Queue();queue.register("send-email", async (job) => {
console.log(job.payload);
// Send email...
});await queue.startWorker();await queue.enqueue({
queue: "emails",
type: "send-email",
payload: {
to: "john@example.com",
subject: "Welcome",
body: "Hello!"
}
});Run a job after a delay.
await queue.enqueue({
queue: "emails",
type: "send-email",
payload: {...},
options: {
delay: 30000
}
});Run a job at a specific time.
await queue.enqueue({
queue: "emails",
type: "send-email",
payload: {...},
options: {
runAt: new Date(Date.now() + 60000)
}
});Higher priority jobs execute before lower priority jobs.
await queue.enqueue({
queue: "emails",
type: "send-email",
payload: {...},
options: {
priority: 100
}
});await queue.enqueue({
queue: "emails",
type: "send-email",
payload: {...},
options: {
maxAttempts: 5
}
});Retries use exponential backoff by default.
const queue = new Queue({
worker: {
concurrency: 5,
pollingInterval: 1000,
heartbeatInterval: 3000,
leaseDuration: 30000
}
});| Option | Default | Description |
|---|---|---|
concurrency |
1 |
Maximum number of jobs processed simultaneously |
pollingInterval |
1000ms |
Database polling interval |
heartbeatInterval |
10000ms |
Lease renewal interval |
leaseDuration |
30000ms |
Visibility timeout (lease duration) |
Retrieve failed jobs.
const jobs = await queue.getDLQ();Replay a failed job.
await queue.replay(jobId);const stats = await queue.stats();Example:
{
"pending": 10,
"processing": 3,
"completed": 250,
"failed": 0,
"dlq": 2
}Run a dedicated scheduler process to recover abandoned jobs.
import { Scheduler } from "@reliable-job-queue/core";
const scheduler = new Scheduler();
await scheduler.start();The scheduler periodically:
- Detects expired leases
- Releases abandoned jobs
- Returns them to the queue
process.on("SIGINT", async () => {
await queue.stopWorker();
});The worker:
- Stops polling
- Waits for running jobs
- Stops renewing leases
- Marks itself offline
Jobs are persisted in PostgreSQL and survive application restarts.
Workers claim jobs using an atomic database update.
Even if multiple workers attempt to claim the same job simultaneously, only one succeeds.
When a worker claims a job:
- Status becomes
PROCESSING - A lease is assigned
- Other workers cannot process it until the lease expires.
Long-running jobs periodically renew their lease to prevent premature recovery.
If a worker crashes while processing a job:
- The lease expires
- The scheduler detects the expired lease
- The job returns to the queue
- Another worker can continue processing it
Jobs exceeding the configured retry limit are automatically moved to the Dead Letter Queue for inspection and replay.
Producer
│
▼
PostgreSQL
┌────────┴────────┐
│ │
▼ ▼
Worker Process Scheduler Process
│ │
Job Executor Recovery Manager
│
User Handler
- Uses PostgreSQL polling for job discovery (LISTEN/NOTIFY is not yet implemented)
- Provides at-least-once delivery semantics; handlers should be idempotent
- Cron-based recurring jobs are not yet supported
const queue = new Queue(options);queue.register(type, handler);
await queue.enqueue(job);
await queue.startWorker();
await queue.stopWorker();
await queue.stats();
await queue.getDLQ();
await queue.replay(jobId);const scheduler = new Scheduler();await scheduler.start();
await scheduler.stop();MIT