Skip to main content
A job that runs every thirty seconds for a week will fail sometimes. A rate limit, a dropped connection, a provider hiccup. The question is not whether it fails but what happens next.

The model

A task that raises is logged and retried. It does not take the schedule down. Under the hood, CronJob polls the underlying schedule library roughly once a second, and schedule only advances a job’s next_run on success — a raised exception leaves the job due immediately, so it gets retried on the next ~1-second poll tick, not paced by the configured interval. That is the only sensible default for something meant to run unattended, but it does mean a failing job can be retried far more often than its interval suggests.
Earlier versions did the opposite: one exception set is_running = False and killed the scheduler thread, while run() returned a job object as though nothing had happened. A single transient failure permanently stopped the job, and the caller was handed a plausible return value and a dead schedule.

Error budgets

Retrying forever is right for transient failures and wrong for a misconfigured job hammering a dead endpoint. max_consecutive_errors draws the line.
  • None (default): never give up. Every failure is logged, every tick retried.
  • An integer: after that many failures in a row, the job stops and run() raises CronJobExecutionError naming the count and the last error.
The counter resets on any success, so a job that fails occasionally never trips the budget. Only a job failing consistently does.
That distinction is the important part: a clean stop() returns, a job that gave up raises. You can tell them apart.

Monitoring while it runs

get_execution_stats() is safe to poll from another thread.

What to watch

error_count on its own is a poor signal: a job running every two seconds for a day will accumulate failures and be perfectly healthy. consecutive_errors is the one that distinguishes noise from breakage.

A complete example

Runs without API keys, since FlakyAgent stands in for an unreliable upstream.
A representative run: 19 successful executions and 21 failures over sixty seconds, never stopping, because the failures never stacked ten deep in a row. That total (40 runs) is higher than a naive “one tick every 2 seconds” model would predict — failures are retried on CronJob’s ~1-second poll loop rather than waiting out the full interval, so a flaky job burns through attempts faster than its configured interval implies.

Budgets across a fleet

With run_many, budgets are per agent. One agent giving up does not stop its siblings:
If the flaky agent exhausts its five, it stops, the stable one keeps running, and run_many raises once blocking ends, naming which job gave up.

Next steps

Multiple Agents

Fleets on mixed cadences

CronJob Quickstart

Start with a single agent

CronJob Reference

Full parameter and method documentation

Runnable Examples

The example files in the repository