variable
cron
Schedule cron jobs.
Call with a callback to run an in-process job, or with a module path and title to register an OS-level job. Bun.cron.parse previews the next fire time; Bun.cron.remove unregisters an OS-level job.
Schedule an in-process cron job that calls a function on a schedule.
Unlike the module-path overload, this runs the callback on the current event loop — the job dies with the process and does not survive reboots. State is shared between invocations (closures, module-level variables, database connections all persist).
| In-process (this overload) | OS-level (path + title) | |
|---|---|---|
| Survives process exit | No | Yes |
| Shared state between runs | Yes | No (fresh process each time) |
| Windows expression limits | None | 48-trigger cap |
| Return type | CronJob (sync) | Promise<void> |
No-overlap guarantee#
The next fire time is computed only after the callback settles (including any returned Promise). If your callback takes 3 minutes and runs every minute, it fires at T+0 → runs until T+3 → next fire is the first minute boundary after T+3. Invocations never stack.
Error semantics#
Matches setTimeout: a synchronous throw emits uncaughtException, a rejected Promise emits unhandledRejection. Without a listener, the process exits with code 1. The job reschedules itself after an error — it does not stop on first failure.
process.on("unhandledRejection", (err) => log.error(err)); // keep going
Bun.cron("* * * * *", async () => { await mightThrow(); });Cron expression syntax#
Five fields: minute hour day-of-month month day-of-week.
| Field | Values | Special chars |
|---|---|---|
| Minute | 0-59 | * , - / |
| Hour | 0-23 | * , - / |
| Day of month | 1-31 | * , - / |
| Month | 1-12 or JAN-DEC | * , - / |
| Day of week | 0-7 or SUN-SAT | * , - / |
0and7both mean Sunday.- Month and weekday names are case-insensitive (
MON,Monday,jan,Januaryall work). - Nicknames:
@yearly,@annually,@monthly,@weekly,@daily,@midnight,@hourly. - When both day-of-month and day-of-week are restricted (neither is
*), the job fires when either matches — POSIX cron OR semantics. - All expressions work on all platforms — there is no Windows trigger limit here.
Lifecycle & --hot#
Under bun --hot, all in-process cron jobs are stopped immediately before the module graph is re-evaluated. Each Bun.cron() call still in your source then re-registers, so editing the schedule, editing the callback, or deleting the line entirely all take effect on save without leaking timers.
By default the job keeps the process alive (like setInterval); call .unref() to let the process exit naturally when nothing else is pending.
Cron expression or nickname (e.g. "*/5 * * * *", "@hourly").
Function to call on each fire. May return a Promise — the next fire is not scheduled until it settles.
A CronJob handle. Chainable: .stop(), .ref(), .unref() all return the job itself.
// Hourly cleanup, keeps process alive
Bun.cron("0 * * * *", async () => {
await cleanupTempFiles();
});
// Background healthcheck that doesn't block process exit
Bun.cron("*/30 * * * *", () => fetch("https://example.com/health")).unref();
// Stop conditionally
const job = Bun.cron("* * * * *", async () => {
if (await isDone()) job.stop();
});Register an OS-level cron job that runs a JavaScript/TypeScript module on a schedule.
Unlike the callback overload, this registers the job with the operating system's scheduler — the job survives process exit and persists across reboots. Bun spawns a fresh process for each invocation, so there is no shared state between runs.
| Platform | Scheduler | Inspect with |
|---|---|---|
| Linux | crontab | crontab -l |
| macOS | launchd | launchctl list |
| Windows | Task Scheduler | schtasks /query |
Module shape#
The target module must have a default export with a scheduled(controller) method, matching the Cloudflare Workers Cron Triggers API. The controller exposes cron (the expression) and scheduledTime (ms since epoch).
// worker.ts
export default {
async scheduled(controller: Bun.CronController) {
console.log(`Fired: ${controller.cron} at ${new Date(controller.scheduledTime)}`);
await doWork();
},
};Cron expression syntax#
Five fields: minute hour day-of-month month day-of-week.
| Field | Values | Special chars |
|---|---|---|
| Minute | 0-59 | * , - / |
| Hour | 0-23 | * , - / |
| Day of month | 1-31 | * , - / |
| Month | 1-12 or JAN-DEC | * , - / |
| Day of week | 0-7 or SUN-SAT | * , - / |
0and7both mean Sunday.- Month and weekday names are case-insensitive (
MON,Monday,jan,Januaryall work). - Nicknames:
@yearly,@annually,@monthly,@weekly,@daily,@midnight,@hourly. - When both day-of-month and day-of-week are restricted (neither is
*), the job fires when either matches — POSIX cron OR semantics.
Platform caveats#
- Windows: minute steps that don't evenly divide 60 (e.g.
*/7,*/11) with all hours active exceed Task Scheduler's 48-trigger limit and throw. Divisors of 60 (*/5,*/10,*/15,*/20,*/30) and all common patterns work. - Windows headless/CI: registration fails if the current user's SID can't be resolved (typical under service accounts). Run as a regular user or create the task manually with
schtasks /create /ru SYSTEM. - macOS: stdout/stderr are written to
/tmp/bun.cron.<title>.{stdout,stderr}.log.
Idempotency & removal#
Registering with a title that already exists replaces the previous entry. Use Bun.cron.remove to unregister. The title is namespaced per user, so different users can register jobs with the same title independently.
Path to the module to run. Resolved relative to the calling file.
Cron expression or nickname (e.g. "30 2 * * MON", "@daily").
Unique identifier for this job. Alphanumeric, hyphens, and underscores only — used directly in crontab markers, launchd service labels, and schtasks task names.
Promise that resolves once the OS scheduler has accepted the job.
// Register once (e.g. in a postinstall script or setup command)
await Bun.cron("./jobs/weekly-report.ts", "30 2 * * MON", "weekly-report");
await Bun.cron("./jobs/cleanup.ts", "@daily", "daily-cleanup");
// Later, to unregister:
await Bun.cron.remove("weekly-report");Parse a cron expression and return the next matching Date in the system's local time zone — the same way crontab, launchd, and Windows Task Scheduler interpret schedules. Pass { tz: "UTC" } (or any IANA time-zone name) to override.
Supports the same syntax as Bun.cron — 5-field expressions, named days/months, and predefined nicknames like @daily.
When both day-of-month and day-of-week are specified (neither is *), matching uses OR logic per POSIX cron: a date matches if either field matches.
DST: spring-forward times shift forward by the gap; in the fall-back duplicated hour, fixed-time schedules fire once (first occurrence) while schedules with * minute or hour fire through both occurrences.
A cron expression or nickname (e.g. "0,15,30,45 * * * *", "0 9 * * MON-FRI", "@hourly")
Starting point for the search (defaults to Date.now()). Accepts a Date or milliseconds since epoch.
{ tz?: string } — IANA time-zone name to interpret the schedule in (defaults to the system's local zone).
The next Date matching the expression, or null if no match exists within 8 years (e.g. "0 0 30 2 *" — Feb 30 never occurs)
// Next weekday at 09:30 local time
const next = Bun.cron.parse("30 9 * * MON-FRI");
// 09:00 in New York, regardless of the server's TZ
const ny = Bun.cron.parse("0 9 * * *", Date.now(), { tz: "America/New_York" });
// Chain calls to get a sequence
const from = new Date();
const first = Bun.cron.parse("@hourly", from);
const second = first ? Bun.cron.parse("@hourly", first) : null;Remove a previously registered cron job by its title.
The title of the cron job to remove
Promise that resolves when the cron job is removed
await Bun.cron.remove("weekly-report");