Bun.cron() in-process scheduling basic usage
Use Bun.cron(schedule, handler) to run a callback on a cron schedule inside the current process. The schedule is a cron expression like '*/5 * * * *' or a predefined nickname like '@hourly'. The handler is called on each fire and may return a Promise. Returns a CronJob synchronously. Throws TypeError if the expression is invalid.
Bun.cron.parse() parse cron expression
Bun.cron.parse(expression, relativeDate?, options?) parses a cron expression and returns the next matching Date in the system's local time zone, or null if no match exists within 8 years. Parameters: expression (string, required) - a 5-field cron expression or predefined nickname; relativeDate (Date | number, optional, defaults to Date.now()) - starting point for search; options (object, optional) with tz field (string, optional) for IANA time-zone name.
Cron expression 5-field syntax
Standard cron format: minute hour day-of-month month day-of-week. Minute: 0–59. Hour: 0–23. Day of month: 1–31. Month: 1–12 or JAN–DEC (case-insensitive). Day of week: 0–7 or SUN–SAT (case-insensitive, both 0 and 7 mean Sunday). Special characters: * (all values), , (list), - (range), / (step).
Cron predefined nicknames
@yearly/@annually = 0 0 1 1 * (once a year, January 1st). @monthly = 0 0 1 * * (once a month, 1st day). @weekly = 0 0 * * 0 (once a week, Sunday). @daily/@midnight = 0 0 * * * (once a day, midnight). @hourly = 0 * * * * (once an hour).
Cron time zone support
Schedules are interpreted in the system's local time zone by default. Override by passing { tz: 'IANA_name' } to Bun.cron.parse() or Bun.cron(). For example, { tz: 'UTC' } or { tz: 'America/New_York' }. OS-level and in-process forms fire at the same wall-clock time.
Cron DST spring-forward behavior
When a schedule lands in the missing hour during spring-forward DST transition, it fires that day shifted forward by the gap. For example, 30 2 * * * runs at 3:30 on the spring-forward day. For multi-minute patterns inside the gap like */15 2 * * *, only the first match fires.
Cron DST fall-back behavior
During fall-back DST transition, a fixed-time schedule in the duplicated hour like 30 1 * * * fires once at the first occurrence. A schedule with * in the minute or hour field like 0 * * * * or * * * * * fires through both occurrences—once per real-time minute, matching crontab on Linux.
Cron day-of-month and day-of-week logic
When both day-of-month and day-of-week are specified (neither is *), the expression matches when either condition is true, following POSIX cron standard. For example, 0 0 15 * FRI fires on the 15th of every month OR every Friday. When only one is specified (the other is *), only that field is used.
Bun.cron() in-process no-overlap guarantee
The next fire time is computed only after the handler—including any returned Promise—settles. If the handler takes 90 seconds and the schedule is * * * * *, the second fire is the first minute boundary after the handler finishes, not 60 seconds after. Invocations never stack.
Bun.cron() in-process error handling
Errors match setTimeout semantics: synchronous throws emit process.on('uncaughtException'); rejected returned Promises emit process.on('unhandledRejection'). Without a listener, the process exits with code 1. With a listener, the job keeps running and retries on the next scheduled time.
CronJob handle properties and methods
CronJob has: job.cron (returns the cron expression string), job.stop() (cancel, handler will not fire again), job.unref() (allow process to exit while scheduled), job.ref() (keep process alive, default). stop(), ref(), and unref() return the job for chaining. CronJob is Disposable: using job = Bun.cron(...) auto-stops at scope exit.
Bun.cron() bun --hot behavior
Under bun --hot, all in-process cron jobs are stopped immediately before module graph re-evaluation. Every Bun.cron() call still in the source re-registers. Editing schedule, handler, or deleting the line entirely all take effect on save without leaking timers.
Bun.cron() fake timers support
In-process cron honors jest.useFakeTimers(). setSystemTime(), advanceTimersByTime(), and runAllTimers() control when it fires, enabling testing scheduled callbacks without waiting on the real clock.
Bun.cron(path, schedule, title) OS-level registration
Register an OS-level cron job: await Bun.cron('./worker.ts', '30 2 * * MON', 'weekly-report'). Parameters: path (string, required) - path to script resolved relative to caller; schedule (string, required) - cron expression or nickname; title (string, required) - unique job identifier using alphanumeric, hyphens, underscores. Re-registering with same title overwrites the existing job in-place.
OS-level cron job scheduled() handler
The registered script must export a default object with a scheduled(controller: Bun.CronController) method. The controller has: controller.cron (the cron expression string), controller.type ('scheduled'), controller.scheduledTime (Date.now() at invocation). The handler can be async; Bun waits for the returned promise to settle before exiting.
Bun.cron() vs in-process vs OS-level comparison
In-process: survives process exit=No, shared state between runs=Yes, platform requirements=None, Windows expression limits=None, returns CronJob. OS-level: survives process exit=Yes, shared state=No (fresh process each time), platform requirements=crontab/launchd/Task Scheduler, Windows expression limits=48-trigger cap, returns Promise<void>.
Linux OS-level cron implementation with crontab
Bun uses crontab to register OS-level jobs. Each job is stored as a line in the user's crontab with a '# bun-cron: <title>' marker comment above it. The crontab entry format: '<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'. View with 'crontab -l'. Logs go to system log: journalctl -u cron or check /var/log/syslog.
macOS OS-level cron implementation with launchd
Bun uses launchd to register OS-level jobs. Each job is installed as a plist file at ~/Library/LaunchAgents/bun.cron.<title>.plist. The plist uses StartCalendarInterval to define the schedule. Complex patterns are expanded into multiple StartCalendarInterval dicts as a Cartesian product. View with 'launchctl list | grep bun.cron'. Logs go to /tmp/bun.cron.<title>.stdout.log and /tmp/bun.cron.<title>.stderr.log.
Windows OS-level cron implementation with Task Scheduler
Bun uses Windows Task Scheduler with CalendarTrigger elements and Repetition patterns. Each job is registered as a scheduled task named 'bun-cron-<title>'. Most cron expressions are fully supported including @daily, @weekly, @monthly, @yearly, ranges, lists, named days/months, and day-of-month patterns. Windows has a 48-trigger limit per task.
Windows cron S4U logon type
Bun registers tasks with S4U (Service-for-User) logon type, which runs jobs as the registering user even when not logged in—matching Linux crontab behavior. No password is stored. TCP/IP networking works normally. The only restriction: S4U tasks cannot access Windows-authenticated network resources like SMB file shares, mapped drives, Kerberos/NTLM services.
Windows cron on headless servers and CI environments
On headless servers and CI where the current user's Security Identifier (SID) cannot be resolved—such as service accounts created by NSSM—Bun.cron() fails with an error. Workaround: run Bun as a regular user account, or create the scheduled task manually with 'schtasks /create /xml <file> /tn <name> /ru SYSTEM /f'.
Windows cron 48-trigger limit examples
Expressions that work: */5 * * * * (1 trigger with Repetition PT5M), */15 * * * * (1 trigger), 0 9 * * MON-FRI (5 triggers, one per weekday), 0,30 9-17 * * * (18 triggers), @daily/@weekly/@monthly/@yearly (1 trigger each). Expressions that fail: */7 * * * * (216 triggers), */8 * * * * (192), */9 * * * * (168), */11 * * * * (144), */13 * * * * (120), */15 * * 6 * (96), 0,30 * 15 * FRI (96).
Windows cron trigger count rules
Minute steps that evenly divide 60 (*/1, */2, */3, */4, */5, */6, */10, */12, */15, */20, */30) use Repetition and work regardless of other fields. Steps that don't divide 60 (*/7, */8, */9, */11, */13, etc.) must expand to individual CalendarTrigger elements; with 24 hours active, the count quickly exceeds 48. Workaround: restrict hour range or use a divisor of 60.
Windows Docker container cron limitation
Bun.cron() OS-level is not supported in Windows Docker containers. The Task Scheduler service is not running in servercore or nanoserver images. Use an in-process scheduler for containerized workloads.
Bun.cron.remove() remove registered cron job
Remove a previously registered OS-level cron job by its title with await Bun.cron.remove('weekly-report'). Works on all platforms. Reverses what Bun.cron() did: on Linux edits crontab, on macOS runs launchctl bootout and deletes plist, on Windows runs schtasks /delete. Removing a job that doesn't exist resolves without error.
Bun.cron.parse() chaining to get sequence of times
Call parse() repeatedly with the result to get a sequence of upcoming times. Example: let cursor = Date.now(); for (let i = 0; i < 3; i++) { cursor = Bun.cron.parse('0 * * * *', cursor)!; console.log(cursor.toLocaleString()); } gets the next three top-of-hour boundaries.
Bun.cron() in-process error handling example
Example showing error handling: process.on('unhandledRejection', err => log.error('cron failed:', err)); Bun.cron('* * * * *', async () => { await mightThrow(); }); Errors are logged and the job retries on the next minute without stopping.
Bun.cron() CronJob Disposable usage
Example using CronJob as Disposable: using job = Bun.cron('0 * * * *', () => {}); automatically stops the job at scope exit. Properties: job.cron returns '0 * * * *', job.stop() cancels it, job.unref() allows process exit, job.ref() keeps process alive.
OS-level cron scheduled() handler example
Example worker.ts: export default { scheduled(controller: Bun.CronController) { console.log(controller.cron); // '30 2 * * 1' console.log(controller.type); // 'scheduled' console.log(controller.scheduledTime); // 1737340201847 (Date.now() at invocation) } };