Why 50 Cron Jobs Was a Red Flag
When I first joined the fintech platform PayPulse, the ops dashboard showed 50 separate cron entries spread across three servers. Each one ran a tiny script: “pull pending payouts”, “reconcile ledger”, “clear stale cache”, and so on. At first glance it looked like a typical “micro‑task” approach, but in production it caused:
- 30 % more CPU spikes during the 2‑minute windows when the jobs overlapped.
- $1,200/month extra on our cloud provider because the instances had to be sized for the worst‑case burst.
- 12 hours/month of on‑call time debugging missed runs, overlapping locks, and time‑zone drift.
Honestly, I knew we needed a single, observable system that could self‑heal when a task failed, retry intelligently, and scale down to zero when idle. The result is the Agent Runner, a Node.js service that loads a list of agent definitions from a JSON file and executes them on a schedule using a lightweight in‑process scheduler.
Core Idea: Agents Instead of Crons
An agent is a small, self‑contained module that exports a single run(context) function. The Agent Runner reads the definition:
{
"name": "payouts",
"schedule": "*/5 * * * *", // every 5 minutes
"module": "./agents/payouts.js",
"maxRetries": 3,
"backoff": "exponential"
}
The runner then:
- Parses the cron expression with cron-parser.
- Schedules the next run with
setTimeout. - Executes the module in a try/catch wrapper.
- On error, respects
maxRetriesand applies the backoff strategy. - Emits a heartbeat event every 30 seconds; a watchdog process restarts any agent that stops emitting.
All agents share a single Node process, but each runs in its own async context, so a blocking bug in one can’t stall the others.
Code Walk‑through – The Scheduler
// scheduler.js
import { CronJob } from 'cron';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import EventEmitter from 'events';
class AgentRunner extends EventEmitter {
constructor(configPath) {
super();
this.agents = JSON.parse(readFileSync(configPath, 'utf8'));
this.jobs = new Map();
}
async start() {
for (const def of this.agents) {
const job = new CronJob(def.schedule, async () => {
await this.executeAgent(def);
});
job.start();
this.jobs.set(def.name, job);
}
this.emit('ready');
}
async executeAgent(def) {
const mod = await import(resolve(def.module));
let attempts = 0;
const run = async () => {
try {
this.emit('heartbeat', def.name);
await mod.run({ logger: console, env: process.env });
this.emit('success', def.name);
} catch (err) {
attempts++;
if (attempts > (def.maxRetries ?? 0)) {
this.emit('failed', def.name, err);
return;
}
const delay = this.backoffMs(attempts, def.backoff);
setTimeout(run, delay);
}
};
run();
}
backoffMs(attempt, type = 'fixed') {
if (type === 'exponential') return 1000 * 2 ** (attempt - 1);
return 3000;
}
stop() {
for (const job of this.jobs.values()) job.stop();
}
}
export default AgentRunner;
The runner lives on a single **t2.medium* instance (2 vCPU, 4 GB RAM). Compared with the previous 3‑node, 6‑core cluster, we shaved $800/month from the bill.*
Self‑Healing Watchdog
A separate tiny script monitors the heartbeat events and restarts any agent that missed a beat for more than 90 seconds.
// watchdog.js
import AgentRunner from './scheduler.js';
const runner = new AgentRunner('./agents.json');
const lastSeen = new Map();
runner.on('heartbeat', name => lastSeen.set(name, Date.now()));
runner.on('ready', () => console.log('Agent Runner ready'));
setInterval(() => {
const now = Date.now();
for (const [name, ts] of lastSeen) {
if (now - ts > 90_000) {
console.warn(`🛠️ Agent ${name} stalled – restarting`);
runner.stop(); // stop all jobs
runner.start(); // restart clean slate
break; // only need one restart
}
}
}, 30_000);
runner.start();
The watchdog runs as a systemd service, guaranteeing that even if the Node process crashes, the OS brings it back up within seconds. Turns out we saw zero missed runs after the first week of deployment.
Real‑World Impact
| Metric | Before Agent Runner | After Agent Runner |
|---|---|---|
| Avg. CPU (peak 5‑min window) | 78 % | 42 % |
| Monthly cloud cost | $2,400 | $1,600 |
| On‑call incidents (per month) | 9 | 2 |
| Time to add a new task | 2 h (edit crontab, deploy) | 15 min (add JSON + module) |
| Mean Time To Recovery (MTTR) | 45 min (manual SSH) | < 2 min (watchdog) |
The biggest win was time saved. Adding a new payout check used to mean editing three separate crontabs, committing, and waiting for the next deploy. Now a teammate drops agents/newCheck.js, adds a line to agents.json, and pushes – the runner picks it up on the next restart (or you can send a SIGHUP to reload instantly). I actually did that last Tuesday and it took me under ten minutes.
Dealing with Legacy Scripts
I didn’t rewrite every existing script from scratch. Most of the old jobs were simple shell wrappers that called a Node module. I wrapped those binaries in a tiny adapter:
// agents/legacyAdapter.js
import { execFile } from 'child_process';
export async function run({ logger }) {
return new Promise((resolve, reject) => {
execFile('bash', ['./legacy/purge.sh'], (err, stdout, stderr) => {
if (err) return reject(err);
logger.info(stdout);
resolve();
});
});
}
That let me migrate all 50 jobs in 3 weeks on our 3‑server setup without breaking downstream dependencies.
Lessons Learned
- Keep the runner stateless – store only heartbeats in memory; if the process restarts, the schedule is rebuilt from the JSON file.
- Use exponential backoff – a flaky external API (e.g., bank gateway) can cause cascading retries; exponential delays keep the system gentle.
- Separate concerns – the watchdog is a different process; never rely on the same event loop for health checks.
-
Instrument everything – I added a Prometheus exporter that tracks
agent_success_total,agent_failure_total, andagent_latency_seconds. Those metrics helped prove the 30 % CPU reduction to finance.
Bottom line: Swapping a zoo of crons for a self‑healing agent system cut our cloud bill by 33 %, halved on‑call noise, and gave us a single place to add new background work.
🔧 Want production‑ready AI agents? Check out AI Agent Kit — 5 agents for $9.
Top comments (0)