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.

function cron(
handler: (this: CronJob) => unknown,
options?: CronOptions

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 exitNoYes
Shared state between runsYesNo (fresh process each time)
Windows expression limitsNone48-trigger cap
Return typeCronJob (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.

FieldValuesSpecial chars
Minute0-59* , - /
Hour0-23* , - /
Day of month1-31* , - /
Month1-12 or JAN-DEC* , - /
Day of week0-7 or SUN-SAT* , - /
  • 0 and 7 both mean Sunday.
  • Month and weekday names are case-insensitive (MON, Monday, jan, January all 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.

@param schedule

Cron expression or nickname (e.g. "*/5 * * * *", "@hourly").

@param handler

Function to call on each fire. May return a Promise — the next fire is not scheduled until it settles.

@returns

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();
});
function cron(
path: string,
title: string
): Promise<void>;

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.

PlatformSchedulerInspect with
Linuxcrontabcrontab -l
macOSlaunchdlaunchctl list
WindowsTask Schedulerschtasks /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.

FieldValuesSpecial chars
Minute0-59* , - /
Hour0-23* , - /
Day of month1-31* , - /
Month1-12 or JAN-DEC* , - /
Day of week0-7 or SUN-SAT* , - /
  • 0 and 7 both mean Sunday.
  • Month and weekday names are case-insensitive (MON, Monday, jan, January all 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.

@param path

Path to the module to run. Resolved relative to the calling file.

@param schedule

Cron expression or nickname (e.g. "30 2 * * MON", "@daily").

@param title

Unique identifier for this job. Alphanumeric, hyphens, and underscores only — used directly in crontab markers, launchd service labels, and schtasks task names.

@returns

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");
function cron.parse(
relativeDate?: number | Date,
options?: CronOptions
): null | Date;

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.

@param expression

A cron expression or nickname (e.g. "0,15,30,45 * * * *", "0 9 * * MON-FRI", "@hourly")

@param relativeDate

Starting point for the search (defaults to Date.now()). Accepts a Date or milliseconds since epoch.

@param options

{ tz?: string } — IANA time-zone name to interpret the schedule in (defaults to the system's local zone).

@returns

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;
function cron.remove(
title: string
): Promise<void>;

Remove a previously registered cron job by its title.

@param title

The title of the cron job to remove

@returns

Promise that resolves when the cron job is removed

await Bun.cron.remove("weekly-report");