Croner: The Universal Cron Library That Runs Everywhere Your JavaScript Does
There is a moment in every developer's career when they need to run a function at 3:15 AM on the second Tuesday of every month, and they think, "How hard can cron be?" The answer, historically, has been: harder than it should be. The Node.js ecosystem has no shortage of cron libraries, but most of them only work in Node.js, drag in external dependencies, or silently skip your jobs during daylight saving time transitions. Croner is the library that decided those tradeoffs were unacceptable.
croner is a zero-dependency cron scheduler written in TypeScript that works identically across Node.js, Deno, Bun, and web browsers. It parses cron expressions with support for seconds, years, and advanced modifiers like last-day-of-month and nth-weekday. It can schedule functions, but it can also simply evaluate cron expressions to enumerate future and past run times without scheduling anything at all. All in-memory, all 6.8 KB gzipped.
What Makes It Tick
Croner packs an impressive feature set into a tiny footprint:
- Universal runtime support across Node.js (18+), Deno (2.0+), Bun (1.0+), and browsers
- Zero external dependencies with a 6.8 KB gzipped bundle
- Extended cron syntax including seconds field, year field, and L/W/#/+ modifiers
- Timezone-aware scheduling with proper DST handling
- Built-in overrun protection that prevents overlapping executions of async jobs
- Pattern enumeration via
nextRuns(),previousRuns(), andmatch()without scheduling anything - Full TypeScript definitions from the ground up
- OCPS 1.4 compliant following the Open Cron Pattern Standard for cross-implementation interoperability
Getting On Schedule
Install via your package manager of choice:
npm install croner
or
yarn add croner
Import the Cron class and you are ready to go:
import { Cron } from "croner";
For Deno users, import directly from JSR:
import { Cron } from "jsr:@hexagon/croner@10.0.1";
Setting Your First Timer
The Classic Cron Job
The API is refreshingly simple. Pass a cron expression and a callback to the Cron constructor, and it starts scheduling immediately:
import { Cron } from "croner";
const job = new Cron("*/5 * * * *", () => {
console.log("This runs every 5 minutes");
});
Need second-level precision? Add a sixth field at the front:
const heartbeat = new Cron("*/10 * * * * *", () => {
console.log("Pinging every 10 seconds");
});
Most cron libraries treat seconds as an afterthought or do not support them at all. Croner makes them a first-class citizen. And if you need year-level control, add a seventh field:
const newYears2027 = new Cron("0 0 0 1 JAN * 2027", () => {
console.log("Happy New Year 2027!");
});
Working Across Time Zones
One of the trickiest parts of cron scheduling is timezone handling. A job set to run at midnight should mean midnight in Tokyo, not midnight in UTC, if that is what your use case demands:
const tokyoReport = new Cron(
"0 0 9 * * *",
{ timezone: "Asia/Tokyo" },
() => {
console.log("Good morning, Tokyo. Generating daily report.");
}
);
Croner uses IANA timezone strings and handles DST transitions correctly. If a scheduled time falls into a DST gap (the clock jumps forward and that time never exists), the job is simply skipped. If it falls into a DST overlap (the clock falls back and the time occurs twice), the job runs once. This sounds obvious, but a surprising number of cron libraries get it wrong.
Controlling the Job Lifecycle
Every Cron instance exposes methods to pause, resume, stop, and force-trigger jobs:
const job = new Cron("0 */30 * * * *", () => {
console.log("Half-hourly task");
});
job.pause();
console.log(job.isRunning()); // false
job.resume();
console.log(job.isRunning()); // true
job.trigger(); // Force an immediate execution
job.stop(); // Permanently stop the job
console.log(job.isStopped()); // true
Pausing is reversible, but stopping is permanent. A stopped job cannot be resumed, so think of stop() as the kill switch.
Beyond the Basics
Pattern Modifiers That Actually Matter
Standard cron expressions are powerful but limited. Need to run something on the last day of every month? The last Friday? The nearest weekday to the 15th? Standard cron cannot express any of these. Croner can:
// Last day of every month at midnight
const monthEnd = new Cron("0 0 L * *", () => {
console.log("End-of-month processing");
});
// Last Friday of every month at 5 PM
const lastFriday = new Cron("0 17 * * FRI#L", () => {
console.log("TGIF -- the last one this month");
});
// Second Monday of every month at 9 AM
const secondMonday = new Cron("0 9 * * MON#2", () => {
console.log("Biweekly-ish board meeting");
});
// Nearest weekday to the 15th of every month
const nearestWeekday = new Cron("0 9 15W * *", () => {
console.log("Mid-month payroll run");
});
The L modifier means "last," # means "nth occurrence," and W means "nearest weekday." These cover enterprise scheduling patterns that would otherwise require custom date math layered on top of simpler cron libraries.
Predicting the Future (and Auditing the Past)
Sometimes you do not want to schedule anything. You just want to know when something would run. Croner doubles as a cron expression evaluator:
const pattern = new Cron("0 0 * * MON");
const nextTenMondays = pattern.nextRuns(10);
console.log("Upcoming Mondays at midnight:", nextTenMondays);
const lastFiveRuns = pattern.previousRuns(5, new Date());
console.log("Last five scheduled runs:", lastFiveRuns);
const christmas = new Date(2026, 11, 25);
const matchesChristmas = pattern.match(christmas);
console.log("Is Christmas a Monday?", matchesChristmas);
This is invaluable for building scheduling UIs, debugging cron expressions, or generating audit logs. The msToNext() method is handy for countdown displays:
const christmasEve = new Cron("0 0 0 24 DEC *");
const ms = christmasEve.msToNext();
if (ms !== null) {
const days = Math.floor(ms / 1000 / 3600 / 24);
console.log(`${days} days until Christmas Eve`);
}
Taming Async Jobs with Overrun Protection
If your cron job calls an API or runs a database migration, there is a real risk that the next scheduled trigger fires while the previous one is still running. Most cron libraries leave this problem to you. Croner has a built-in protect option:
const syncJob = new Cron(
"*/30 * * * * *",
{
protect: (job) => {
console.warn(`Skipped trigger -- previous run of ${job.getPattern()} still in progress`);
},
},
async () => {
const data = await fetchExternalAPI();
await processAndStore(data);
}
);
Set protect to true to silently skip overlapping runs, or pass a callback to be notified when an overrun is blocked. This single option eliminates an entire class of bugs that plague production cron systems.
Named Jobs and the Global Registry
For applications with many scheduled tasks, tracking jobs by reference can get unwieldy. Named jobs solve this by registering themselves in a global scheduledJobs array:
import { Cron, scheduledJobs } from "croner";
new Cron("0 */5 * * * *", { name: "healthCheck" }, () => {
checkSystemHealth();
});
new Cron("0 0 2 * * *", { name: "nightlyBackup" }, async () => {
await runBackup();
});
console.log(scheduledJobs.length); // 2
console.log(scheduledJobs.map((j) => j.name)); // ["healthCheck", "nightlyBackup"]
When a named job is stopped, it automatically removes itself from the registry. This makes it straightforward to build admin dashboards that list, monitor, and control all active jobs in your application.
Guarding with Limits and Boundaries
Croner provides several options for constraining when and how often jobs execute:
const limitedJob = new Cron(
"* * * * * *",
{
maxRuns: 5,
startAt: "2026-03-01T00:00:00",
stopAt: "2026-03-31T23:59:59",
catch: (error) => {
console.error("Job failed:", error);
},
},
() => {
console.log("Runs at most 5 times, only during March 2026");
}
);
The maxRuns option caps total executions. The startAt and stopAt options define a time window. The catch option prevents unhandled promise rejections from crashing your process, which is especially important for async callbacks that might throw.
How It Stacks Up
The JavaScript cron ecosystem has four major players: croner, cron, node-cron, and node-schedule. Here is the short version: croner is the only one that works in browsers and Deno, has zero dependencies, and supports advanced pattern modifiers. Both node-cron and node-schedule have over a hundred open issues and have gone quiet on maintenance. The cron package is actively maintained but pulls in Luxon as a dependency and does not work outside Node.js.
At roughly 4.3 million weekly downloads, Croner is now neck-and-neck with the cron package in popularity, but it does more with less: zero dependencies, a 6.8 KB gzipped bundle versus 28.2 KB, and benchmarks showing 160,000+ operations per second versus 6,000 for cron.
Wrapping Up
Croner is what happens when someone looks at the cron library landscape and decides that "good enough" is not. It runs everywhere, weighs almost nothing, handles edge cases that most alternatives punt on, and does it all with zero dependencies. The v10 release brought year-field support, pattern matching, previous-run enumeration, and rock-solid DST handling, pushing it further ahead of the pack.
If your scheduling needs go beyond simple Node.js timers -- if you need browser support, second-level precision, timezone awareness, overrun protection, or the ability to ask "when is the last Friday of next month?" without writing date math by hand -- croner is the tool that has you covered. Install it, write your cron expression, and let the clock do the rest.