Guides
Running in production
Cronbake is a timer in your process, and most production concerns follow from that. Here's what to plan for.
On this page
One scheduler per app
Every instance of your app that calls bakeAll() runs every job. With three replicas, your nightly report goes out three times. Cronbake has no distributed lock, so pick one of these approaches.
Run the scheduler in one place. Either a dedicated worker process, or an environment variable set on a single instance.
if (process.env.RUN_SCHEDULER === "true") {
baker.bakeAll();
}Let every instance try, and take a lock in the callback. This keeps working if an instance goes down, at the cost of needing somewhere to keep the lock. Here’s the idea using Redis:
baker.add({
name: "nightly-report",
cron: "@daily",
callback: async () => {
const acquired = await redis.set("lock:nightly-report", "1", "EX", 3600, "NX");
if (!acquired) return;
await sendNightlyReport();
},
});Time zones
Schedules follow the local time zone of the process, and there’s no per-job setting. @daily means midnight wherever the process thinks it is. Set the zone when you start the process, so it doesn’t depend on the machine.
TZ=UTC bun run server.tsUTC is usually the safest choice for servers. If a job has to run at a local time for your users, such as 09:00 in New York, run a separate process with that TZ, or schedule more often and check the time in the callback.
Daylight saving
If the process uses a zone with daylight saving, jobs scheduled inside the changeover hour behave differently on those two days. Here’s what happens in America/New_York:
| Change | Schedule | Result |
|---|---|---|
| Clocks go forward | 0 30 2 * * * |
Doesn’t run that day, because 02:30 doesn’t exist |
| Clocks go back | 0 30 1 * * * |
Runs twice that day, because 01:30 happens twice |
Jobs outside that window aren’t affected. Running in UTC avoids the problem altogether.
Shutting down
stopAll() prevents new runs but doesn’t wait for the ones in progress, and exiting straight away cuts them off. There’s no built-in way to drain them, but tracking them yourself only takes a few lines.
const inFlight = new Set<Promise<void>>();
function tracked(work: () => Promise<void>) {
return () => {
const run = work().finally(() => inFlight.delete(run));
inFlight.add(run);
return run;
};
}
baker.add({
name: "sync-orders",
cron: "@every_5_minutes",
callback: tracked(syncOrders),
});
process.once("SIGTERM", async () => {
baker.stopAll();
await Promise.allSettled(inFlight);
await baker.saveState(); // if you use persistence
process.exit(0);
});A scheduled job’s timer keeps the process alive. If your process is meant to exit when its work is done, stop or destroy the jobs first.
Missed ticks
Nothing makes up for a tick that didn’t fire. That includes ticks that fall while the process is down, while a job is paused, or while the event loop is blocked. The job carries on from the next tick after the current time.
If a job has to catch up after downtime, record its last successful run somewhere durable, and have the callback work out what it missed. Or use immediate: true so that it checks once on startup.
baker.add({
name: "send-digests",
cron: "@hourly",
immediate: true,
callback: async () => {
const since = await loadLastDigestTime();
await sendDigestsSince(since);
},
});Keeping the event loop free
Callbacks run on the main thread with the rest of your app. Async work is fine, but a callback that spends seconds on CPU blocks your server and delays other jobs’ timers. Move heavy computation to a worker thread or a separate process, and call that from the callback.
What isn’t included
So you can plan around them, Cronbake doesn’t have:
- Distributed locking
- Retries, which you can add with a wrapper
- Concurrency limits across jobs
- Per-job time zones
- A way to wait for running callbacks, though you can track them yourself
- Timeouts for a callback that never finishes
That last one deserves attention. With overlap protection on, a callback that hangs forever will silently skip every later tick. If a job calls something that can hang, give that call a timeout, and alert on skippedExecutions.