Distributed, Redis-backed cron scheduler with multi-tenant guardrails and jitter, designed for Node.js 24 and TypeScript.
npm install redis-distro-schedulerDistributed, Redis-backed cron scheduler with multi-tenant guardrails and jitter,
designed for Node.js 24 and TypeScript.
- ✅ Works across multiple pods / processes (Redis lock)
- ✅ Dynamic create / update / delete of schedules (no restart)
- ✅ Per-tenant guardrails (limit schedules, enforce min interval)
- ✅ Jitter to avoid thundering herd on the same second
- ✅ First-class TypeScript support
``bash`
npm install redis-distro-scheduleror
yarn add redis-distro-scheduler
- A schedule is a row in your DB (per customer) that defines:
- cronExpressiontimezone
-
- report payload (what to generate, recipients, filters, etc.)
- Each schedule is executed by the library via a Scheduler instance.
- SchedulerManager keeps all schedulers in memory and reacts to:
- initial load from DB
- create / update / delete events (e.g. via Redis Pub/Sub)
- Redis is used as a distributed lock, so only one pod runs a job per tick.
`ts
import type { ScheduleRecord } from "redis-distro-scheduler";
// Example structure (you likely already have something similar in your DB)
// interface ScheduleRecord {
// id: string;
// customerId: string;
// name: string;
// cronExpression: string;
// timezone?: string;
// payload: any;
// enabled: boolean;
// }
`
`ts
import { SchedulerManager, type ScheduleRecord } from "redis-distro-scheduler";
async function fetchAllEnabledSchedules(): Promise
// SELECT * FROM report_schedules WHERE enabled = 1;
return [];
}
async function callback(schedule: ScheduleRecord): Promise
// Your business logic:
// - Generate report
// - Send email
// - Store file / etc.
console.log("Running report for schedule", schedule.id);
}
const manager = new SchedulerManager({
redisUrl: process.env.REDIS_URL,
callback,
defaultMaxJitterMs: 30_000, // spread executions up to 30s per schedule
});
// On startup
await manager.loadInitialSchedules(await fetchAllEnabledSchedules);
`
`ts
type ScheduleEvent =
| { type: "created" | "updated"; schedule: ScheduleRecord }
| { type: "deleted"; scheduleId: string };
await sub.subscribe("report-schedules");
sub.on("message", (_channel, msg) => {
const event: ScheduleEvent = JSON.parse(msg);
switch (event.type) {
case "created":
case "updated":
manager.upsertSchedule(event.schedule);
break;
case "deleted":
manager.removeSchedule(event.scheduleId);
break;
}
});
`
Use ensureMinimumInterval to enforce per-tenant cron frequency limits:
`ts
import {
DEFAULT_TENANT_GUARDRAILS,
ensureMinimumInterval,
} from "redis-distro-scheduler";
async function validateScheduleBeforeSave(input: {
cronExpression: string;
timezone?: string;
customerId: string;
}) {
// Example: enforce minimum interval
ensureMinimumInterval(
input.cronExpression,
input.timezone,
DEFAULT_TENANT_GUARDRAILS.MIN_INTERVAL_MINUTES
);
// Example: count active schedules in DB and compare to
// DEFAULT_TENANT_GUARDRAILS.MAX_ACTIVE_SCHEDULES_PER_CUSTOMER
}
`
- Each schedule has a dedicated Redis key:
`txt`
redis-distro-scheduler:report:
- At each cron tick:
1. All pods try SET key "locked" NX PX ."OK"
2. Only one pod gets and runs the job.maxJitterMs
3. That pod waits a deterministic jitter based on schedule ID, up to .callback(schedule)
4. It then executes your callback.
High-level orchestrator for many schedules.
`ts
const manager = new SchedulerManager(callback: async (schedule) => {
// ...
}, {
redisUrl: process.env.REDIS_URL
});
await manager.loadInitialSchedules(fetchAllEnabled);
// React to events:
manager.upsertSchedule(schedule); // create or update
manager.removeSchedule(id); // delete / disable
`
`ts`
import {
DEFAULT_TENANT_GUARDRAILS,
ensureMinimumInterval,
type TenantGuardrailsConfig,
} from "redis-distro-scheduler";
- Written in TypeScript, compiled to ESM + CJS.
- Tested with Node.js 24.
- Ships with type declarations (.d.ts).
This package uses Vitest for unit tests.
`bash``
npm test
MIT