Skip to main content
Bun has built-in support for registering OS-level cron jobs and parsing cron expressions.

Quickstart

Parse a cron expression to find the next matching time:
// Next weekday at 9:30 AM UTC
const next = Bun.cron.parse("30 9 * * MON-FRI");
console.log(next); // => 2025-01-20T09:30:00.000Z
Register a cron job that runs a script on a schedule:
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

Bun.cron.parse()

Parse a cron expression and return the next matching UTC Date.
const next = Bun.cron.parse("*/15 * * * *");
console.log(next); // => next quarter-hour boundary

Parameters

ParameterTypeDescription
expressionstringA 5-field cron expression or predefined nickname
relativeDateDate | numberStarting point for the search (defaults to Date.now())

Returns

Date | null — the next matching UTC time, or null if no match exists within ~4 years (e.g. February 30th).

Chaining calls

Call parse() repeatedly to get a sequence of upcoming times:
const from = Date.UTC(2025, 0, 15, 10, 0, 0);

const first = Bun.cron.parse("0 * * * *", from);
console.log(first); // => 2025-01-15T11:00:00.000Z

const second = Bun.cron.parse("0 * * * *", first);
console.log(second); // => 2025-01-15T12:00:00.000Z

Cron expression syntax

Standard 5-field format: minute hour day-of-month month day-of-week
FieldValuesSpecial characters
Minute059* , - /
Hour023* , - /
Day of month131* , - /
Month112 or JANDEC* , - /
Day of week07 or SUNSAT* , - /

Special characters

CharacterDescriptionExample
*All values* * * * * — every minute
,List1,15 * * * * — minute 1 and 15
-Range9-17 * * * * — minutes 9 through 17
/Step*/15 * * * * — every 15 minutes

Named values

Month and weekday fields accept case-insensitive names:
// 3-letter abbreviations
Bun.cron.parse("0 9 * * MON-FRI"); // weekdays
Bun.cron.parse("0 0 1 JAN,JUN *"); // January and June

// Full names
Bun.cron.parse("0 9 * * Monday-Friday");
Bun.cron.parse("0 0 1 January *");
Both 0 and 7 mean Sunday in the weekday field.

Predefined nicknames

NicknameEquivalentDescription
@yearly / @annually0 0 1 1 *Once a year (January 1st)
@monthly0 0 1 * *Once a month (1st day)
@weekly0 0 * * 0Once a week (Sunday)
@daily / @midnight0 0 * * *Once a day (midnight)
@hourly0 * * * *Once an hour
const next = Bun.cron.parse("@daily");
console.log(next); // => next midnight UTC

Day-of-month and day-of-week interaction

When both day-of-month and day-of-week are specified (neither is *), the expression matches when either condition is true. This follows the POSIX cron standard.
// Fires on the 15th of every month OR every Friday
Bun.cron.parse("0 0 15 * FRI");
When only one is specified (the other is *), only that field is used for matching.

Bun.cron()

Register an OS-level cron job that runs a JavaScript/TypeScript module on a schedule.
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

Parameters

ParameterTypeDescription
pathstringPath to the script (resolved relative to caller)
schedulestringCron expression or nickname
titlestringUnique job identifier (alphanumeric, hyphens, underscores)
Re-registering with the same title overwrites the existing job in-place — the old schedule is replaced, not duplicated.
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // every hour
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // replaces: every 15 min

The scheduled() handler

The registered script must export a default object with a scheduled() method, following the Cloudflare Workers Cron Triggers API:
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)
  },
};
The handler can be async. Bun waits for the returned promise to settle before exiting.

How it works per platform

Linux

Bun uses crontab to register jobs. Each job is stored as a line in your user’s crontab with a # bun-cron: <title> marker comment above it. The crontab entry looks like:
<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'
When the cron daemon fires the job, Bun imports your module and calls the scheduled() handler. Viewing registered jobs:
crontab -l
Logs: On Linux, cron output goes to the system log. Check with:
# systemd-based (Ubuntu, Fedora, Arch, etc.)
journalctl -u cron       # or crond on some distros
journalctl -u cron --since "1 hour ago"

# syslog-based (older systems)
grep CRON /var/log/syslog
To capture stdout/stderr to a file, redirect output in the crontab entry directly, or add logging inside your scheduled() handler. Manually uninstalling without code:
# Edit your crontab and remove the "# bun-cron: <title>" comment
# and the command line below it
crontab -e

# Or remove ALL bun cron jobs at once by filtering them out:
crontab -l | grep -v "# bun-cron:" | grep -v "\-\-cron-title=" | crontab -

macOS

Bun uses launchd to register 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 with ranges, lists, or steps are supported — Bun expands them into multiple StartCalendarInterval dicts via Cartesian product. Viewing registered jobs:
launchctl list | grep bun.cron
Logs: stdout and stderr are written to:
/tmp/bun.cron.<title>.stdout.log
/tmp/bun.cron.<title>.stderr.log
For example, a job titled weekly-report:
cat /tmp/bun.cron.weekly-report.stdout.log
tail -f /tmp/bun.cron.weekly-report.stderr.log
Manually uninstalling without code:
# Unload the job from launchd
launchctl bootout gui/$(id -u)/bun.cron.<title>

# Delete the plist file
rm ~/Library/LaunchAgents/bun.cron.<title>.plist

# Example for a job titled "weekly-report":
launchctl bootout gui/$(id -u)/bun.cron.weekly-report
rm ~/Library/LaunchAgents/bun.cron.weekly-report.plist

Windows

Bun uses Windows Task Scheduler with XML-based task definitions. Each job is registered as a scheduled task named bun-cron-<title> using CalendarTrigger elements and Repetition patterns. Most cron expressions are fully supported, including @daily, @weekly, @monthly, @yearly, ranges (1-5), lists (1,15), named days/months, and day-of-month patterns.

User context

Tasks are registered using 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 (fetch(), HTTP, WebSocket, database connections) works normally. The only restriction is that S4U tasks cannot access Windows-authenticated network resources (SMB file shares, mapped drives, Kerberos/NTLM services). On headless servers and CI environments where the current user’s Security Identifier (SID) cannot be resolved — such as service accounts created by NSSM or similar tools — Bun.cron() will fail with an error explaining the issue. To work around this, either run Bun as a regular user account, or create the scheduled task manually with schtasks /create /xml <file> /tn <name> /ru SYSTEM /f.

Trigger limit

:::caution Windows Task Scheduler enforces a limit of 48 triggers per task (the CalendarTrigger element has maxOccurs="48"). Some cron expressions that work on Linux and macOS exceed this limit on Windows. Expressions that work on all platforms:
PatternTrigger strategyCount
*/5 * * * *Single trigger with Repetition (PT5M)1
*/15 * * * *Single trigger with Repetition (PT15M)1
0 9 * * MON-FRIOne CalendarTrigger per weekday5
0,30 9-17 * * *2 minutes × 9 hours18
@daily, @weekly, @monthly, @yearlySingle trigger1
Expressions that fail on Windows (but work on Linux and macOS):
PatternWhyTrigger count
*/7 * * * *9 minute values × 24 hours216
*/8 * * * *8 minute values × 24 hours192
*/9 * * * *7 minute values × 24 hours168
*/11 * * * *6 minute values × 24 hours144
*/13 * * * *5 minute values × 24 hours120
*/15 * * 6 *Month restriction prevents Repetition: 4 × 2496
0,30 * 15 * FRIOR-split doubles triggers: 2 × 24 × 296
The key factor is whether the expression can use a Repetition interval (single trigger) or must expand to individual CalendarTrigger elements. 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 be expanded, and with 24 hours active, the count quickly exceeds 48. When a pattern exceeds the limit, Bun.cron() rejects it with an error message. To work around it, simplify the expression or restrict the hour range:
// ❌ Fails on Windows: */7 with all hours = 216 triggers
await Bun.cron("./job.ts", "*/7 * * * *", "my-job");

// ✅ Works: restrict to specific hours (9 values × 5 hours = 45 triggers)
await Bun.cron("./job.ts", "*/7 9-13 * * *", "my-job");

// ✅ Works: use a divisor of 60 instead (Repetition, 1 trigger)
await Bun.cron("./job.ts", "*/5 * * * *", "my-job");
:::

Windows containers

:::caution Bun.cron() 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. ::: Viewing registered jobs:
schtasks /query /tn "bun-cron-<title>"

# List all bun cron tasks
schtasks /query | findstr "bun-cron-"
Manually uninstalling without code:
schtasks /delete /tn "bun-cron-<title>" /f

# Example:
schtasks /delete /tn "bun-cron-weekly-report" /f
Or open Task Scheduler (taskschd.msc), find the task named bun-cron-<title>, right-click, and delete it.

Bun.cron.remove()

Remove a previously registered cron job by its title. Works on all platforms.
await Bun.cron.remove("weekly-report");
This reverses what Bun.cron() did:
PlatformWhat remove() does
LinuxEdits crontab to remove the entry and its marker comment
macOSRuns launchctl bootout and deletes the plist file
WindowsRuns schtasks /delete to remove the scheduled task
Removing a job that doesn’t exist resolves without error.