Redis
A dedicated notification worker without BullMQ. The dispatcher pushes serialized jobs onto a Redis list; a long-running worker drains it with a blocking BRPOP loop and isolates per-job failures.
Want out-of-process delivery on a dedicated worker, but you're not running BullMQ? The Redis
dispatcher is a lean alternative. It pushes serialized jobs onto a Redis list, and a long-running
worker pops and delivers them — no queue framework, just ioredis.
Like BullMQ, it crosses a process boundary, so it serializes the job and rehydrates it on the worker. See Async dispatch.
Install
pnpm add @dudousxd/nestjs-notifications-redis ioredisnpm install @dudousxd/nestjs-notifications-redis ioredisWire it up
Import RedisDispatcherModule.forRoot() to provide the client, dispatcher, and worker, then set
RedisNotificationDispatcher as the dispatcher. Add the notifications registry and
resolveNotifiable the worker needs to rebuild jobs.
import { Module } from '@nestjs/common';
import { NotificationsModule } from '@dudousxd/nestjs-notifications-core';
import {
RedisDispatcherModule,
RedisNotificationDispatcher,
} from '@dudousxd/nestjs-notifications-redis';
import { InvoicePaid } from './invoice-paid.notification';
@Module({
imports: [
NotificationsModule.forRoot({
dispatcher: RedisNotificationDispatcher,
imports: [
RedisDispatcherModule.forRoot({ connection: 'redis://localhost:6379' }),
],
notifications: [InvoicePaid],
resolveNotifiable: (ref) => users.findById(ref.id), // reload the recipient
}),
],
})
export class AppModule {}resolveNotifiable usually needs an injected repository — use forRootAsync when it does.
Connection options
RedisDispatcherModule.forRoot() takes a RedisDispatcherOptions:
interface RedisDispatcherOptions {
/** ioredis connection: a URL string or a host/port (+optional password) object. */
connection: string | { host: string; port: number; password?: string };
/** Ready-job queue (Redis list). Defaults to `nestjs-notifications:jobs`. */
key?: string;
/** Delayed-job sorted set (durable across restarts). Defaults to `nestjs-notifications:scheduled`. */
scheduledKey?: string;
/** Dead-letter list for terminally-failed jobs. Defaults to `nestjs-notifications:dead`. */
deadLetterKey?: string;
/** Delivery attempts before a job is dead-lettered. `1` = no retry. Defaults to `3`. */
maxAttempts?: number;
/** How often (ms) the worker polls the scheduled set for due jobs. Defaults to `1000`. */
pollIntervalMs?: number;
}The dispatcher LPUSHes serialized jobs onto key; the worker BRPOPs from the same key.
Override key if you run multiple independent queues against one Redis instance.
Durable scheduling, retries & dead-letter
The dispatcher is durable, not fire-and-forget:
- Scheduled jobs survive restarts. Delayed sends are stored in a Redis sorted set
(
scheduledKey, score = fire time); a poller moves due jobs onto the ready queue everypollIntervalMs. A process restart no longer loses a pendingsetTimeout. - Failed jobs retry, then dead-letter. The worker delivers a job up to
maxAttemptstimes (default3); a job that exhausts them isLPUSHed to the dead-letter queue (deadLetterKey) instead of being dropped. Inspect or replay it from there.
RedisDispatcherModule.forRoot({
connection: 'redis://localhost:6379',
maxAttempts: 5, // retry up to 5× before dead-lettering
deadLetterKey: 'myapp:notifications:dead',
});maxAttempts: 1 reproduces the old behavior of no retry — except the failed job is now
dead-lettered rather than silently dropped, so nothing is lost.
The dedicated worker
RedisNotificationWorker is a long-running worker that drains the list and delivers
notifications. Key behaviours:
- Blocking loop. It runs a
BRPOPloop on its own ioredis connection, separate from the dispatcher's client, so a blocking pop never starves other commands. - Per-job error isolation. A single failing job is logged and skipped — the loop keeps running. Notifications don't get stuck behind one bad payload.
- Clean shutdown. On module destroy it stops the loop and disconnects, unblocking any
in-flight
BRPOPimmediately.
The module registers both the dispatcher and the worker, so it works whether you run one process or split API and worker.
This dispatcher is for a dedicated worker without BullMQ. If you already run BullMQ, prefer the BullMQ dispatcher and reuse that infrastructure instead.
The worker must run somewhere — in the same process, or a separate worker process that imports
RedisDispatcherModule.forRoot() along with NotificationsModule and your channel modules. If
nothing drains the list, jobs accumulate undelivered.
BullMQ
Out-of-process delivery on a BullMQ worker, reusing your app's existing @nestjs/bullmq and Redis connection. Jobs serialize, enqueue with retry and backoff, and rehydrate on the worker.
Recipes
Task-focused guides for the things you actually reach for — on-demand sends, queueing end-to-end, writing your own channel, and wiring up Telescope.