Aviary
Dispatchers

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 ioredis
npm install @dudousxd/nestjs-notifications-redis ioredis

Wire 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.

app.module.ts
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:

redis-options.ts
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 every pollIntervalMs. A process restart no longer loses a pending setTimeout.
  • Failed jobs retry, then dead-letter. The worker delivers a job up to maxAttempts times (default 3); a job that exhausts them is LPUSHed to the dead-letter queue (deadLetterKey) instead of being dropped. Inspect or replay it from there.
app.module.ts
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 BRPOP loop 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 BRPOP immediately.

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.

On this page