I wanted to run scheduled jobs on macOS, but launchd is still hard to read, and I wanted an easier path, so I decided to use PM2 instead.
Configuration for running a Cron job with PM2
cd $PROJECT_ROOT
pm2 ecosystemManage the configuration in ecosystem.config.cjs.
module.exports = { apps: [ { name: "cronJob", script: "main.ts", interpreter: "~/.bun/bin/bun", // number of instances is 1 instances: 1, // run every 15 minutes cron_restart: "*/15 * * * *", exec_mode: "fork", // restart automatically when target files change: no watch: false, // restart automatically: no autorestart: false, }, ],};Points
- cron_restart: set the interval in cron format
- autorestart: false: since this is a cron-style job, automatic restart is disabled
- interpreter: you can specify any runtime, including Bun
Start command:
pm2 start ecosystem.config.cjs
pm2 saveSample job implementation
This sample periodically fetches the IP address and stores it in a database.
import { Database } from 'bun:sqlite';
const DDL = `CREATE TABLE IF NOT EXISTS ip_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);`
async function fetchIpConfig() { const response = await fetch("https://ifconfig.io/ip").catch((err) => new Error(err)); if (response instanceof Error || !response.ok) { return null; }
const ip = await response.text();
return ip;}
async function main() { const ip = await fetchIpConfig(); if (ip == null) return;
using db = new Database('ip-monitor.sqlite', { create: true }); db.exec(DDL); using query = db.query("INSERT INTO ip_history (ip) VALUES ($ip);"); query.run({ $ip: ip });}
await main();References
- PM2 - Ecosystem File
- How to create Cron jobs with PM2 | NullNull
- How to make a task job with PM2? - Stack Overflow
Summary
With PM2, cron-style jobs can be managed with a simpler setup than launchd.
Define the job in ecosystem.config.cjs, then use PM2’s process management features for logs and restarts when needed.
hsb.horse