Guides
Persistence
Save job definitions to a file, Redis or your own store and load them again on startup. Functions can't be saved, so your code still registers each job's callback.
On this page
- Turning it on
- What happens on restart
- Things to watch for
- Re-adding a job doesn’t keep it running
- Re-adding a job resets its metrics
- Removing a job from your code doesn’t remove it from the state
- A restore never overwrites a job you’ve added
- When state is saved
- What gets saved
- Keeping a job out of the state
- Restoring by hand
- When loading fails
- What it doesn’t do
Jobs live in memory by default and are gone when the process exits. For many apps that’s fine, because the jobs are defined in code and get added again on every start. Persistence is useful when jobs are created at runtime, for example from an admin page, and their schedules need to outlive the process.
Turning it on
import Baker from "cronbake";
const baker = Baker.create({
persistence: {
enabled: true,
filePath: "./data/cronbake.json",
},
});
await baker.ready();
baker.add({
name: "nightly-report",
cron: "@daily",
start: true,
callback: sendNightlyReport,
});filePath defaults to ./cronbake-state.json, and its parent folders are created as needed. See persistence providers to use Redis or your own store.
What happens on restart
- While it’s being created, the baker starts reading the saved state.
await baker.ready()waits for this to finish. - Each saved job is recreated with its name, schedule, priority, metrics and history. If it was saved as
running, it starts straight away. - Restored jobs have a placeholder callback, because functions can’t be saved. It only logs a warning:
Restored job 'nightly-report' executed but no callback was provided. - When you call
add()with the name of a restored job, your definition replaces it completely, including the schedule.
So a restored job doesn’t do any useful work until your code adds it again with a real callback. For jobs created at runtime, save enough information to rebuild the callback, and map each restored name back to its function during startup.
Things to watch for
Re-adding a job doesn’t keep it running
The replacement is a new job, and it follows the normal start rules. Even if the restored job was running, the new one stays stopped unless you pass start: true, use autoStart, or call bakeAll().
Re-adding a job resets its metrics
await baker.ready();
const previousRuns = baker.getHistory("nightly-report");
await archive(previousRuns);
baker.add({ name: "nightly-report", cron: "@daily", start: true, callback: sendNightlyReport });Removing a job from your code doesn’t remove it from the state
A job you’ve deleted from your code is still in the saved state, so it comes back on the next start with its placeholder callback. If it was running, it will log a warning on every tick. Remove it once to clear it.
await baker.ready();
baker.remove("old-job-name");A restore never overwrites a job you’ve added
restoreState() skips any name that already exists, so your definition wins whether you add the job before or after the restore. Awaiting ready() first is still the clearer way to write it, and it’s the only way to read a job’s saved history before replacing it.
When state is saved
The baker saves automatically after add(), remove(), destroy() and destroyAll(). It doesn’t save after bake(), stop(), pause(), resume() or their bulk versions, and it doesn’t save after a run.
This matters because the saved status decides whether a restored job starts. If you add jobs and then call bakeAll(), the last save still says stopped. Call saveState() whenever you want the file to catch up, and always on shutdown.
baker.bakeAll();
await baker.saveState();
// Keep metrics and status fresh while the app runs
baker.add({
name: "save-cronbake-state",
cron: "@every_5_minutes",
persist: false,
start: true,
callback: () => baker.saveState(),
});
process.once("SIGTERM", async () => {
baker.stopAll();
await baker.saveState();
process.exit(0);
});Writes are queued, so overlapping saves can’t interleave. If an automatic save fails, the error goes to logger.warn(). A saveState() you call yourself rejects instead.
What gets saved
| Saved | Not saved |
|---|---|
name, cron, priority |
callback, onTick, onComplete, onError |
| Current status | immediate, delay, overrunProtection |
| Metrics and history | maxHistory, logger |
Restored jobs use the default for anything that isn’t saved, until you add them again.
{
"version": 1,
"timestamp": "2026-11-04T09:30:00.000Z",
"jobs": [
{
"name": "nightly-report",
"cron": "@daily",
"status": "running",
"priority": 0,
"persist": true,
"metrics": {
"totalExecutions": 1,
"successfulExecutions": 1,
"failedExecutions": 0,
"averageExecutionTime": 412
},
"history": [
{ "timestamp": "2026-11-04T00:00:00.000Z", "duration": 412, "success": true }
]
}
],
"config": {
"pollingInterval": 1000,
"useCalculatedTimeouts": true,
"maxHistoryEntries": 100
}
}config is written for reference but isn’t read back. Scheduler settings always come from the options you pass to Baker.create().
Keeping a job out of the state
Set persist: false on a job and it’s never saved. It defaults to true when persistence is on, and has no effect when persistence is off.
baker.add({
name: "local-debug",
cron: "@every_10_seconds",
persist: false,
callback: printDebugInfo,
});Restoring by hand
autoRestore defaults to true. Turn it off to decide when loading happens.
const baker = Baker.create({
persistence: { enabled: true, autoRestore: false },
});
await connectToEverything();
await baker.restoreState();When loading fails
If the provider’s load() throws or the saved state is malformed, ready() rejects, and autoStart doesn’t start anything.
The file provider is more forgiving. A missing, unreadable or corrupt file counts as no saved state, so the baker starts empty with no error or warning. If those jobs matter, check that they came back with getJobNames().
What it doesn’t do
Persistence isn’t coordination. Two processes sharing a file or a Redis key overwrite each other’s state, with the last write winning, and neither takes a lock on running a job. Saved state also doesn’t make up for downtime, so ticks that passed while the process was stopped aren’t replayed.