-
Notifications
You must be signed in to change notification settings - Fork 2
Cronus
A cross-runtime, minute-resolution cron scheduler for Deno, Bun, and Node.js.
Cronus runs on all five targets — it is plain setTimeout with no
server-only imports, so it loads and ticks on Deno, Bun, Node.js,
Cloudflare Workers, and the browser alike. A minute-resolution
scheduler is most useful on a long-lived process, though: it only
earns its keep where something keeps the process alive between ticks. A
browser tab can close at any time, and a standard Cloudflare Worker has
no background execution between requests — a Durable Object's Alarms
API is a different scheduling model. Reach for it on a server.
Cronus runs jobs on standard 5-field cron expressions using a tick-and-match architecture: a self-correcting timer fires at each minute boundary and runs every job whose schedule matches the current time. It never computes a "next run", which has three practical consequences:
- An impossible expression (
0 0 30 2 *— Feb 30) simply never fires instead of crashing the scheduler. - There is no far-future timer to overflow — a "fires in 40 days" schedule is just a match that eventually comes true.
- Drift self-corrects every tick (the next tick targets the next
:00boundary, notnow + 60s).
Jobs never overlap themselves: while a job's action is running, matching
ticks are skipped — a job scheduled every minute that takes five minutes
to run resumes on the sixth minute. The package is dependency-light: it
imports @tundralibs/utils (base error and event classes) and
@tundralibs/compat (cross-runtime timer unref).
| Module | Description | Documentation |
|---|---|---|
Cronus |
The scheduler — jobs, ticker, events, run-now | This page |
| Schedules | Cron expression syntax and matching semantics | Cronus-Schedule-Syntax |
| Jobs | Job lifecycle, overlap prevention, events | Cronus-Jobs |
./errors |
CronusError plus InvalidScheduleError etc. |
Cronus-Errors |
./types |
CronusJobInfo, CronusRunContext, ParsedSchedule … |
— |
Deno:
deno add @tundralibs/cronusBun:
bunx jsr add @tundralibs/cronusNode.js:
npx jsr add @tundralibs/cronusimport { Cronus } from '@tundralibs/cronus';
declare function purgeExpired(): Promise<void>;
declare function runMigration(): Promise<void>;
const cron = new Cronus();
// Observability — actions and listeners can throw without harm:
cron.on(
'error',
(_runId, name, _at, _ms, err) =>
console.error(`job ${name} failed:`, err.message),
);
cron.on('skip', (name) => console.warn(`${name} still running — tick skipped`));
// Recurring: every hour on the hour.
cron.add('hourly-cleanup', '0 * * * *', async () => {
await purgeExpired();
});
// Once: next 03:00, then auto-removes.
cron.addOnce('migrate', '0 3 * * *', runMigration);
cron.start();
// Run-now, bypassing the schedule (false if already running):
await cron.trigger('hourly-cleanup');By default the ticker holds the event loop (standalone-daemon
friendly). When embedding inside a host that owns the lifecycle (an
HTTP server), pass { unref: true } so a pending tick never blocks
shutdown — and call stop() on teardown.
With
unref: trueand nothing else holding the loop, the process can exit mid-run of an async job — cronus never blocks shutdown for you. The host is responsible for draining in-flight work (e.g. tracking outstanding job promises and awaiting them) before exit.
import { Cronus } from '@tundralibs/cronus';
const cron = new Cronus({ unref: true });- Cronus-Schedule-Syntax - Cron expression fields, names, steps, and POSIX matching semantics
- Cronus-Jobs - Job lifecycle, overlap prevention, run-once/run-now, and the event surface
- Cronus-Errors - The typed error hierarchy
MIT