https://github.com/boringnode/queue
A simple and efficient framework-agnostic queue system for Node.js applications
https://github.com/boringnode/queue
framework-agnostic nodejs queue queue-workers
Last synced: about 5 hours ago
JSON representation
A simple and efficient framework-agnostic queue system for Node.js applications
- Host: GitHub
- URL: https://github.com/boringnode/queue
- Owner: boringnode
- License: mit
- Created: 2024-09-14T10:35:22.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-01-26T19:01:05.000Z (about 19 hours ago)
- Last Synced: 2026-01-27T06:37:29.435Z (about 8 hours ago)
- Topics: framework-agnostic, nodejs, queue, queue-workers
- Language: TypeScript
- Homepage:
- Size: 374 KB
- Stars: 87
- Watchers: 5
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: .github/funding.yml
- License: LICENSE.md
Awesome Lists containing this project
README
# @boringnode/queue
[![typescript-image]][typescript-url]
[![gh-workflow-image]][gh-workflow-url]
[![npm-image]][npm-url]
[![npm-download-image]][npm-download-url]
[![license-image]][license-url]
A simple and efficient queue system for Node.js applications. Built for simplicity and ease of use, `@boringnode/queue` allows you to dispatch background jobs and process them asynchronously with support for multiple queue adapters.
## Installation
```bash
npm install @boringnode/queue
```
## Features
- **Multiple Queue Adapters**: Redis, Knex (PostgreSQL, MySQL, SQLite), and Sync
- **Type-Safe Jobs**: TypeScript classes with typed payloads
- **Delayed Jobs**: Schedule jobs to run after a delay
- **Priority Queues**: Process high-priority jobs first
- **Bulk Dispatch**: Efficiently dispatch thousands of jobs at once
- **Job Grouping**: Organize related jobs for monitoring
- **Retry with Backoff**: Exponential, linear, or fixed backoff strategies
- **Job Timeout**: Fail or retry jobs that exceed a time limit
- **Job History**: Retain completed/failed jobs for debugging
- **Scheduled Jobs**: Cron or interval-based recurring jobs
- **Auto-Discovery**: Automatically register jobs from specified locations
## Quick Start
### 1. Define a Job
```typescript
import { Job } from '@boringnode/queue'
import type { JobOptions } from '@boringnode/queue/types'
interface SendEmailPayload {
to: string
}
export default class SendEmailJob extends Job {
static options: JobOptions = {
queue: 'email',
}
async execute(): Promise {
console.log(`Sending email to: ${this.payload.to}`)
}
}
```
> [!NOTE]
> The job name defaults to the class name (`SendEmailJob`). You can override it with `name: 'CustomName'` in options.
> [!WARNING]
> If you minify your code in production, class names may be mangled. Always specify `name` explicitly in your job options.
### 2. Configure the Queue Manager
```typescript
import { QueueManager } from '@boringnode/queue'
import { redis } from '@boringnode/queue/drivers/redis_adapter'
await QueueManager.init({
default: 'redis',
adapters: {
redis: redis({ host: 'localhost', port: 6379 }),
},
locations: ['./app/jobs/**/*.ts'],
})
```
### 3. Dispatch Jobs
```typescript
// Simple dispatch
await SendEmailJob.dispatch({ to: 'user@example.com' })
// With options
await SendEmailJob.dispatch({ to: 'user@example.com' })
.toQueue('high-priority')
.priority(1)
.in('5m')
```
### 4. Start a Worker
```typescript
import { Worker } from '@boringnode/queue'
const worker = new Worker(config)
await worker.start(['default', 'email'])
```
## Bulk Dispatch
Efficiently dispatch thousands of jobs in a single batch operation:
```typescript
const { jobIds } = await SendEmailJob.dispatchMany([
{ to: 'user1@example.com' },
{ to: 'user2@example.com' },
{ to: 'user3@example.com' },
])
.group('newsletter-jan-2025')
.toQueue('emails')
.priority(3)
console.log(`Dispatched ${jobIds.length} jobs`)
```
This uses Redis MULTI/EXEC or SQL batch insert for optimal performance.
## Job Grouping
Organize related jobs together for monitoring and filtering:
```typescript
// Group newsletter jobs
await SendEmailJob.dispatch({ to: 'user@example.com' }).group('newsletter-jan-2025')
// Group with bulk dispatch
await SendEmailJob.dispatchMany(recipients).group('newsletter-jan-2025')
```
The `groupId` is stored with job data and accessible via `job.data.groupId`.
## Job History & Retention
Keep completed and failed jobs for debugging:
```typescript
export default class ImportantJob extends Job {
static options: JobOptions = {
// Keep last 1000 completed jobs
removeOnComplete: { count: 1000 },
// Keep failed jobs for 7 days
removeOnFail: { age: '7d' },
}
}
```
Retention options
| Value | Behavior |
| --------------------------- | ------------------ |
| `true` (default) | Remove immediately |
| `false` | Keep forever |
| `{ count: n }` | Keep last n jobs |
| `{ age: '7d' }` | Keep for duration |
| `{ count: 100, age: '1d' }` | Both limits apply |
Query job history:
```typescript
const job = await adapter.getJob('job-id', 'queue-name')
console.log(job.status) // 'completed' | 'failed'
console.log(job.finishedAt) // timestamp
console.log(job.error) // error message (if failed)
```
## Adapters
### Redis (recommended for production)
```typescript
import { redis } from '@boringnode/queue/drivers/redis_adapter'
// With options
const adapter = redis({ host: 'localhost', port: 6379 })
// With existing ioredis instance
import { Redis } from 'ioredis'
const connection = new Redis({ host: 'localhost' })
const adapter = redis(connection)
```
### Knex (PostgreSQL, MySQL, SQLite)
```typescript
import { knex } from '@boringnode/queue/drivers/knex_adapter'
const adapter = knex({
client: 'pg',
connection: { host: 'localhost', database: 'myapp' },
})
```
More Knex examples
```typescript
// With existing Knex instance
import Knex from 'knex'
const connection = Knex({ client: 'pg', connection: '...' })
const adapter = knex(connection)
// Custom table name
const adapter = knex(config, 'custom_jobs_table')
```
Database setup with QueueSchemaService
The Knex adapter requires tables to be created before use. Use `QueueSchemaService` to create them:
```typescript
import { QueueSchemaService } from '@boringnode/queue'
import Knex from 'knex'
const connection = Knex({ client: 'pg', connection: '...' })
const schemaService = new QueueSchemaService(connection)
// Create tables with default names
await schemaService.createJobsTable()
await schemaService.createSchedulesTable()
// Or extend with custom columns
await schemaService.createJobsTable('queue_jobs', (table) => {
table.string('tenant_id', 255).nullable()
})
```
**AdonisJS migration example:**
```typescript
import { BaseSchema } from '@adonisjs/lucid/schema'
import { QueueSchemaService } from '@boringnode/queue'
export default class extends BaseSchema {
async up() {
const schemaService = new QueueSchemaService(this.db.connection().getWriteClient())
await schemaService.createJobsTable()
await schemaService.createSchedulesTable()
}
async down() {
const schemaService = new QueueSchemaService(this.db.connection().getWriteClient())
await schemaService.dropSchedulesTable()
await schemaService.dropJobsTable()
}
}
```
### Fake (testing + assertions)
```typescript
import { QueueManager } from '@boringnode/queue'
import { redis } from '@boringnode/queue/drivers/redis_adapter'
await QueueManager.init({
default: 'redis',
adapters: {
redis: redis({ host: 'localhost' }),
},
locations: ['./app/jobs/**/*.ts'],
})
const adapter = QueueManager.fake()
await SendEmailJob.dispatch({ to: 'user@example.com' })
adapter.assertPushed(SendEmailJob)
adapter.assertPushed(SendEmailJob, {
queue: 'default',
payload: (payload) => payload.to === 'user@example.com',
})
adapter.assertPushedCount(1)
QueueManager.restore()
```
### Sync (for testing)
```typescript
import { sync } from '@boringnode/queue/drivers/sync_adapter'
const adapter = sync() // Jobs execute immediately
```
## Job Options
```typescript
export default class MyJob extends Job {
static options: JobOptions = {
queue: 'email', // Queue name (default: 'default')
priority: 1, // Lower = higher priority (default: 5)
maxRetries: 3, // Retry attempts before failing
timeout: '30s', // Max execution time
failOnTimeout: true, // Fail permanently on timeout (default: retry)
removeOnComplete: { count: 100 }, // Keep last 100 completed
removeOnFail: { age: '7d' }, // Keep failed for 7 days
}
}
```
## Delayed Jobs
```typescript
await SendEmailJob.dispatch(payload).in('30s') // 30 seconds
await SendEmailJob.dispatch(payload).in('5m') // 5 minutes
await SendEmailJob.dispatch(payload).in('2h') // 2 hours
await SendEmailJob.dispatch(payload).in('1d') // 1 day
```
## Retry & Backoff
```typescript
import { exponentialBackoff } from '@boringnode/queue'
export default class ReliableJob extends Job {
static options: JobOptions = {
maxRetries: 5,
retry: {
backoff: () =>
exponentialBackoff({
baseDelay: '1s',
maxDelay: '1m',
multiplier: 2,
jitter: true,
}),
},
}
}
```
Available strategies
```typescript
import { exponentialBackoff, linearBackoff, fixedBackoff } from '@boringnode/queue'
// Exponential: 1s, 2s, 4s, 8s...
exponentialBackoff({ baseDelay: '1s', maxDelay: '1m', multiplier: 2 })
// Linear: 1s, 2s, 3s, 4s...
linearBackoff({ baseDelay: '1s', maxDelay: '30s', multiplier: 1 })
// Fixed: 5s, 5s, 5s...
fixedBackoff({ baseDelay: '5s', jitter: true })
```
## Job Timeout
```typescript
export default class LongRunningJob extends Job {
static options: JobOptions = {
timeout: '30s',
failOnTimeout: false, // Will retry (default)
}
async execute(): Promise {
for (const item of this.payload.items) {
// Check abort signal for graceful timeout handling
if (this.signal?.aborted) {
throw new Error('Job timed out')
}
await this.processItem(item)
}
}
}
```
## Job Context
Access execution metadata via `this.context`:
```typescript
async execute(): Promise {
console.log(this.context.jobId) // Unique job ID
console.log(this.context.attempt) // 1, 2, 3...
console.log(this.context.queue) // Queue name
console.log(this.context.priority) // Priority value
console.log(this.context.acquiredAt) // When acquired
console.log(this.context.stalledCount) // Stall recoveries
}
```
## Scheduled Jobs
Run jobs on a recurring basis:
```typescript
// Every 10 seconds
await MetricsJob.schedule({ endpoint: '/health' }).every('10s')
// Cron schedule
await CleanupJob.schedule({ days: 30 })
.id('daily-cleanup')
.cron('0 0 * * *') // Midnight daily
.timezone('Europe/Paris')
```
Schedule management
```typescript
import { Schedule } from '@boringnode/queue'
// Find and manage
const schedule = await Schedule.find('daily-cleanup')
await schedule.pause()
await schedule.resume()
await schedule.trigger() // Run now
await schedule.delete()
// List schedules
const all = await Schedule.list()
const active = await Schedule.list({ status: 'active' })
```
**Schedule options:**
| Method | Description |
| ------------------- | --------------------------------- |
| `.id(string)` | Unique identifier |
| `.every(duration)` | Fixed interval ('5s', '1m', '1h') |
| `.cron(expression)` | Cron schedule |
| `.timezone(tz)` | Timezone (default: 'UTC') |
| `.from(date)` | Start boundary |
| `.to(date)` | End boundary |
| `.limit(n)` | Maximum runs |
## Dependency Injection
Integrate with IoC containers:
```typescript
await QueueManager.init({
// ...
jobFactory: async (JobClass) => {
return app.container.make(JobClass)
},
})
```
Example with injected services
```typescript
export default class SendEmailJob extends Job {
constructor(
private mailer: MailerService,
private logger: Logger
) {
super()
}
async execute(): Promise {
this.logger.info(`Sending email to ${this.payload.to}`)
await this.mailer.send(this.payload)
}
}
```
## Worker Configuration
```typescript
const config = {
worker: {
concurrency: 5, // Parallel jobs
idleDelay: '2s', // Poll interval when idle
timeout: '1m', // Default job timeout
stalledThreshold: '30s', // When to consider job stalled
stalledInterval: '30s', // How often to check
maxStalledCount: 1, // Max recoveries before failing
gracefulShutdown: true, // Wait for jobs on SIGTERM
},
}
```
## Logging
```typescript
import { pino } from 'pino'
await QueueManager.init({
// ...
logger: pino(),
})
```
## Benchmarks
Performance comparison with BullMQ (5ms simulated work per job):
| Jobs | Concurrency | @boringnode/queue | BullMQ | Diff |
| ---- | ----------- | ----------------- | ------ | ----------- |
| 1000 | 5 | 1096ms | 1116ms | 1.8% faster |
| 1000 | 10 | 565ms | 579ms | 2.4% faster |
| 100K | 10 | 56.2s | 57.5s | 2.1% faster |
| 100K | 20 | 29.1s | 29.6s | 1.7% faster |
```bash
npm run benchmark -- --realistic
```
[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/boringnode/queue/checks.yml?branch=main&style=for-the-badge
[gh-workflow-url]: https://github.com/boringnode/queue/actions/workflows/checks.yml
[npm-image]: https://img.shields.io/npm/v/@boringnode/queue.svg?style=for-the-badge&logo=npm
[npm-url]: https://www.npmjs.com/package/@boringnode/queue
[npm-download-image]: https://img.shields.io/npm/dm/@boringnode/queue?style=for-the-badge
[npm-download-url]: https://www.npmjs.com/package/@boringnode/queue
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: https://www.typescriptlang.org
[license-image]: https://img.shields.io/npm/l/@boringnode/queue?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md