Bun.cron schedules a TypeScript job without node-cron

1 month ago

A five-field cron expression can already run inside Bun. This was run on Bun 1.3. The program asks Bun.cron for the next quarter-hour match, and Bun returns a JavaScript Date. No node-cron package is installed. That parse call is the first step toward a real scheduled TypeScript job.

Ask about this presentation

Chapters

  1. 0:00Bun.cron can calculate the next run inside Bun
  2. 0:2001 Install starts with a Bun project
  3. 0:4002 First run parses one schedule string
  4. 1:0603 Bun.cron has three jobs on one docs page
  5. 1:3004 The in-process job shares the live Bun process
  6. 2:03Bun waits before scheduling the next callback
  7. 2:25The OS-level job imports a module later
  8. 3:07Choose the job by what the job must keep
  9. 3:3505 Gotcha one: Bun.cron uses five fields
  10. 3:58Gotcha two: OS jobs need a real path string
  11. 4:30Gotcha three: registration is separate from the first run
Show transcript
  1. A five-field cron expression can already run inside Bun. This was run on Bun 1.3. The program asks Bun.cron for the next quarter-hour match, and Bun returns a JavaScript Date. No node-cron package is installed. That parse call is the first step toward a real scheduled TypeScript job.

    Slide text
    Bun.cron can calculate the next run inside Bun
    Bun 1.3 local run
    parse-and-handle.ts
    const expression = "*/15 * * * *";
    const next = Bun.cron.parse(
      expression,
      new Date("2026-09-23T12:00:00-04:00")
    );
    console.log(next?.toISOString());
    bun assets/source/parse-and-handle.ts
    expression */15 * * * *
    next 2026-09-23T16:15:00.000Z
    handle.cron 0 0 1 1 *
    typeof stop function
    typeof ref function
    typeof unref function
    typeof next undefined
    stopped
    Source: Local Bun 1.3 run, 2026-09-23 · assets/terminal/01-parse-and-handle.txt
  2. The setup is the normal Bun setup. Bun runs TypeScript files directly, so the smallest cron project does not need a compiler step before the first run. The docs page we are walking is Runtime, Process and System, Cron. The API on this page is Bun.cron, Bun’s built-in scheduler API.

    Slide text
    01
    Install
    01 Install starts with a Bun project
    runtime Bun runs TypeScript
    check bun --version
    docs Runtime, Process and System, Cron
    Source: Bun Cron docs, Quickstart · assets/screenshots/docs-quickstart.png
  3. A cron expression is a schedule string with five fields. The fields are minute, hour, day of month, month, and weekday. In this first run, the expression says every fifteen minutes. The star in the other four fields means every hour, every day of month, every month, and every weekday. We pass noon Eastern on September twenty-third as the reference time. Bun returns four fifteen PM UTC, which is the same instant as twelve fifteen PM Eastern.

    Slide text
    02
    First run
    02 First run parses one schedule string
    assets/source/parse-and-handle.ts
    console.log("bun", Bun.version);
    
    const expression = "*/15 * * * *";
    const next = Bun.cron.parse(
      expression,
      new Date("2026-09-23T12:00:00-04:00")
    );
    console.log("expression", expression);
    console.log("next", next?.toISOString());
    
    const job = Bun.cron("0 0 1 1 *", function () {});
    console.log("handle.cron", job.cron);
    console.log("typeof stop", typeof job.stop);
    job.stop();
    terminal output
    expression */15 * * * *
    next 2026-09-23T16:15:00.000Z
    handle.cron 0 0 1 1 *
    typeof stop function
    typeof ref function
    typeof unref function
    typeof next undefined
    stopped
    Source: Local Bun run, parse-and-handle.ts · assets/source/parse-and-handle.ts
  4. The docs split Bun.cron into three related jobs. The parse form turns a schedule into the next matching Date. The in-process form runs a callback inside the Bun process that is already alive. The OS-level form registers a TypeScript module with the operating system, so the operating system starts Bun later. That split is the shape of the API.

    Slide text
    03
    Shape
    03 Bun.cron has three jobs on one docs page
    parse expression to Date
    cron schedule plus handler
    cron path plus schedule plus title
    Source: Bun Cron docs · parse(expression), cron(schedule, handler), cron(path, schedule, title)
  5. The first real job uses the in-process form. The schedule is built for the next minute. Bun.cron registers a callback. When the minute arrives, the callback prints the current time, prints the cron string on the handle, then calls stop. A CronJob handle is the object Bun returns so your code can stop, ref, or unref the job. The callback uses a normal function instead of an arrow function because Bun binds the handle as `this`. In this capture, stop returns the same job object, and the process exits cleanly after the first fire.

    Slide text
    04
    Real thing
    04 The in-process job shares the live Bun process
    assets/source/in-process-once.ts
    const now = new Date();
    const minute = (now.getUTCMinutes() + 1) % 60;
    const hour = minute === 0
      ? (now.getUTCHours() + 1) % 24
      : now.getUTCHours();
    const schedule = `${minute} ${hour} * * *`;
    
    const job = Bun.cron(schedule, function (this: Bun.CronJob) {
      console.log("fired", new Date().toISOString());
      console.log("handle.cron", this.cron);
      console.log("stop result is same job", this.stop() === this);
      process.exit(0);
    });
    terminal output
    schedule 30 12 * * *
    registered 2026-09-23T12:29:42.017Z
    fired 2026-09-23T12:30:00.002Z
    handle.cron 30 12 * * *
    stop result is same job true
    Source: Local Bun run, in-process-once.ts · assets/terminal/02b-in-process-once-utc-schedule.txt
  6. The in-process form has a no-overlap guarantee in the docs. Bun waits for the handler to finish before it computes the next fire time. If the handler returns a promise, Bun waits for that promise to settle. That means a slow callback does not stack another copy on top of itself. Cleanup jobs and cache refresh jobs usually need that single-copy behavior.

    Slide text
    Bun waits before scheduling the next callback
    1 handler starts
    2 handler settles
    3 next fire time is computed
    Source: Bun Cron docs, No-overlap guarantee · assets/screenshots/docs-in-process.png
  7. The OS-level form trades live app state for persistence. The worker exports a scheduled function. Bun registers that file with the operating system. The controller argument gives the worker the cron string, the scheduler type, and the scheduled time. On macOS, the operating system piece is launchd. On Linux, the docs say Bun writes a crontab entry. On Windows, the docs say Bun writes a Task Scheduler task. The local capture registered a macOS LaunchAgents plist. The plist points back to Bun, passes the cron title and cron period, and names the worker TypeScript file. The capture checked that launchd knew about the job, then removed the plist and booted the job out.

    Slide text
    The OS-level job imports a module later
    OS registration and cleanup
    title manual-cron-stage03-1790166456
    registered ~/Library/LaunchAgents/bun.cron.manual-cron-stage03-1790166456.plist
    plist exists yes
    "ProgramArguments" => [
      "/Users/siderakis/.bun/bin/bun"
      "run"
      "--cron-title=manual-cron-stage03-1790166456"
      "--cron-period=0 0 1 1 *"
      "/Users/siderakis/code/YouTube/.../assets/source/worker.ts"
    ]
    launchctl before cleanup
    - 0 bun.cron.manual-cron-stage03-1790166456
    cleanup command launchctl bootout gui/501/bun.cron.manual-cron-stage03-1790166456
    plist remains no
    Source: Local macOS launchd registration, cleaned up after capture · assets/terminal/03-os-registration-and-cleanup.txt
  8. Use an in-process job when the scheduled work needs the server’s memory, database pool, cache, or feature flags that already live in the process. That is the shape for a cache refresher inside a long-running server. Use an OS-level job when the scheduled work must survive a server restart or a reboot. That is the shape for a report generator or a cleanup script that can start fresh. The price is that the OS-level job starts a fresh Bun process, so shared in-memory state is gone.

    Slide text
    Choose the job by what the job must keep
    Question
    In-process job
    OS-level job
    Shares module state
    yes
    no
    Survives process exit
    no
    yes
    Started by
    Bun process
    operating system
    Good first use
    cache refresh
    report generator
    Source: Bun Cron docs · In-process scheduling and OS-level scheduling sections
  9. This is the part people get wrong with seconds. Bun.cron uses five fields today. The first field is minutes. If you bring over a six-field schedule from another library and expect the first field to mean seconds, you are teaching Bun a shape the docs do not show. Bun issue twenty-nine four forty-seven asks for seconds granularity, and that issue is still open in the captured data.

    Slide text
    05
    Gotchas
    05 Gotcha one: Bun.cron uses five fields
    Source: Bun docs and oven-sh/bun issue #29447 · seconds granularity request
  10. This is the part people get wrong with paths. The OS-level form wants a path that Bun resolves relative to the caller. The docs example uses a plain path string. Issue twenty-eight two ninety-five reports that `import.meta.resolve("./worker.ts")` produces a file URL, and that file URL fails with a path resolution error. That is easy to hit because import.meta.resolve is a common JavaScript habit for module paths. Use the plain path unless the issue has been fixed in the Bun version you are running.

    Slide text
    Gotcha two: OS jobs need a real path string
    plain path ./worker.ts
    file URL import.meta.resolve("./worker.ts")
    reported failure path resolution error
    Source: oven-sh/bun issue #28295 · file URL path gotcha
  11. This is the part people get wrong with registration. Registering an OS-level job does not also run the job immediately. Issue twenty-eight two ninety-seven asks for a RunAtLoad-style option, and the captured issue is open. One Windows Server report, issue thirty-four one ninety-five, says a repeating OS-level Bun.cron task stayed idle until the next midnight. Treat that as one reported Windows scheduler edge case, then test your target host. For the first job, use the in-process form when the job belongs to your running app. Use the OS-level form when the job needs to come back after the process is gone. The runnable pieces are `parse-and-handle.ts`, `in-process-once.ts`, and `worker.ts`.

    Slide text
    Gotcha three: registration is separate from the first run
    closing snippet
    Bun.cron(
      "./worker.ts",
      "*/15 * * * *",
      "cache-refresh"
    );
    in-process
    use it when the job belongs to your running app
    OS-level
    use it when the job needs to come back after the process is gone
    Source: oven-sh/bun issues #28297 and #34195 · closing code from Bun.cron OS-level form

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built with ❤️ in New York