All about crons
Want to schedule a recurring command on a node? This article explains how to create and configure crons in Core, and the behaviour worth knowing once they're running.
Where to find crons
Crons are managed per UNIX user. In Core, open a UNIX user — the 'Crons' section lists its crons and offers a '+' button to create one.
Creating a cron
The 'Create Cron' modal opens with a 'Type' dropdown:
- 'Custom command' — write any command yourself.
- 'Schedule (Laravel)' — pre-fills
php …/artisan schedule:run, schedules every minute, and turns off locking (Laravel handles its own withwithoutOverlapping). - 'WP-Cron (WordPress)' — pre-fills
wp --path=… cron event run --due-now, schedules every minute, leaves locking on.
The remainder of the modal is the same regardless of type, with an 'Show advanced options' expandable for less-common fields.
Standard fields
- 'Name' — lowercase, digits, dash, underscore. Identifies the cron.
- 'Command' — what to run. The command itself may not call
exit— let your script's normal exit code propagate. - 'Email Address' — where failure notifications go. Leave empty to send to Cyberfusion.
- 'Schedule' — preset (every 5 minutes / minute / hour / 4 hours / day) or 'Custom' for a free-form cron expression.
- 'Locking Enabled' — prevents two instances of the same cron from running at once. On by default. Turn off only if the command handles its own locking.
- 'Active' — toggles whether the schedule actually fires.
- 'Node' — which node runs the cron.
Advanced options
- 'Error Count' — send an email after this many consecutive failures. Counter resets after any successful run. Default '1' (email after every failure).
- 'Random Delay (s)' — randomise each run's start by up to this many seconds. Use to stagger crons that share a schedule, so they don't all hit the node at the same instant. Default '10'.
- 'Timeout (s)' — kill the cron after this many seconds. A failsafe for commands that may unexpectedly hang (e.g. blocked external API calls). Without a timeout, a hung run can sit forever — and (if locking is on) block every subsequent scheduled run.
- 'Memory Limit (MiB)' — kill the cron if it exceeds this. Minimum 256.
- 'CPU Limit (cores)' — throttle the cron to at most this many CPU cores.
Behaviour worth knowing
How this differs from system cron
Traditional system cron (the one bundled with Linux) emails you whenever the command produces any output — on stdout or stderr — regardless of whether the command actually succeeded. The exit code is ignored. The usual workaround is to append > /dev/null 2>&1 to the command so nothing reaches the inbox, which also throws away any output you'd want for debugging.
Cyberfusion's crons work the other way around:
- Output is always written to a log file on the node — see "Where to find logs of previous runs" below.
- Emails fire only when the command exits with a non-zero return code, never just because it printed something.
This means you can log freely from your script — echo, print, framework loggers — without that triggering a failure email, and you can still read everything back from the log afterwards. It also means the /dev/null redirect isn't just unnecessary, it actively hides debug output you'll want when something does fail (see "Don't redirect to /dev/null" below).
What counts as a failure
A cron run has failed when the command exits with a return code other than 0. Failure emails contain that return code along with the command's output.
Suppressing one-off failures with "Error Count"
By default, you get an email after every single failed run. The 'Error Count' advanced option changes that threshold — set it to a number greater than 1 and Cyberfusion only emails after that many consecutive failures. The counter resets after any successful run.
Use it when the cron is allowed to fail occasionally and you only want to be told about sustained breakage:
- Legacy projects that fail every now and then but recover on the next run — you only want to know when something's been broken for a while.
- Crons that depend on flaky external APIs — a single failed call shouldn't email you; ten in a row should.
Example: with 'Error Count' set to '3', a cron has to fail three runs in a row before you get an email. A single failure followed by a successful run sends nothing.
This is separate from the hard 10-failure cap below — that one still applies on top, regardless of what you set 'Error Count' to.
Stop-spamming guard
If a cron fails more than 10 times consecutively, Cyberfusion stops sending failure emails until the cron succeeds again. This prevents an indefinitely broken cron from flooding your inbox.
Don't redirect to /dev/null
Don't append > /dev/null to the command. Notifications fire only on a non-zero exit, not on output — so there's nothing to suppress. The 'Create Cron' modal flags this with a warning if it detects the redirection.
Command failures and "exited with return code" emails
The most common failure email. You get one when the command itself exits with a non-zero return code (and it didn't hit a timeout), with this in the body:
===== FATAL =====
Cron script command exited with return code <N>.
===== FATAL =====
The number is the exit code the command returned.
To work out what went wrong, look at the lines above that block in the email. Everything between Running cron command and the ===== FATAL ===== line is whatever the command itself wrote to stdout/stderr during the failed run — a PHP stack trace, a wp-cli error, a command not found, and so on. That's where the actual error message lives.
If those lines are empty, the command produced no output before exiting — there's nothing more to go on from the email alone. Either add logging to the script, or run the command manually from a shell session to reproduce the failure.
Locking and "still active" emails
Locking is on by default — each scheduled run holds a lock until it finishes, and the next run won't start while that lock is held. When the lock is still held at the next scheduled time, that run doesn't execute. You get a failure email instead, with this in the body:
===== FATAL =====
<PID> is still active. Stopping execution.
===== FATAL =====
The number is the process ID of the run that's still going.
The cause is always the same: the cron takes longer to complete than its schedule allows. Example: scheduled every 5 minutes, but a run takes 7 minutes — the run due at the 5-minute mark refuses to start, because the previous one is still going.
Three ways to fix it:
- Schedule the cron less often, or make the command finish faster, so each run reliably completes within the interval.
- Set a 'Timeout (s)'. Without one, a stuck run can hold the lock forever — every subsequent scheduled run will send a "still active" email until the stuck process is killed.
- Turn 'Locking Enabled' off if the command handles concurrency itself. Laravel's scheduler is the common case:
withoutOverlappingalready prevents overlap, so Cyberfusion's lock is redundant and only produces these emails. The 'Schedule (Laravel)' cron type turns locking off automatically for this reason.
If a run is already stuck and you can't wait it out, contact Cyberfusion support — we can kill the stuck process for you, so the cron starts running on its schedule again.
Timeouts and "timed out" emails
When a cron has a 'Timeout (s)' set and a run exceeds it, Cyberfusion kills the run and sends a failure email with this in the body:
timeout: sending signal TERM to command '<path>'
===== FATAL =====
Cron script timed out
===== FATAL =====
The run took longer than the timeout allows, so it was killed before it could finish — either to free up resources, or (when locking is on) to stop a stuck run from blocking every scheduled run after it.
If you see this regularly, either raise the 'Timeout (s)' to match how long the command actually needs, or fix what's making the command slow (typically a slow external API, a database lock, or unexpected load on the node).
Where to find logs of previous runs
Every cron run is logged on the node, under cronscripts/logs/ in the UNIX user's home directory. Open the directory over SSH or SFTP as the UNIX user.
Each cron keeps a rotating set of files, named after the cron:
<name>.log (most recent finished run)
<name>.log.0 (the one before)
<name>.log.1
<name>.log.2
…
A file currently being written to — i.e., a run that's still going — has a .log.TMP-<pid>.<suffix> name instead. If you see one for a given cron, that cron is running right now.
Use these to answer questions like "did this cron actually run last night?" or "what did the run an hour ago print?" — without waiting for, or relying on, a failure email.
PHP and Node.js versions
The cron runs as the UNIX user — which means it inherits that user's default PHP and Node.js versions. A bare php or node in your command always resolves to the version set on the UNIX user, the same way an interactive shell session would. See UNIX user namespacing for how that works.
Use cases by target group
Where crons land per group
- Web agencies — the WP-Cron and Laravel Scheduler types cover almost every client site you'll host. Both presets set the schedule and the locking correctly without any tweaking — you don't have to remember the convention per CMS.
- Shops — daily product feed exports, stock sync to marketplaces, abandoned-cart reminders. Set a sensible 'Timeout (s)' so a third-party API outage can't leave the cron stuck running for hours (and, with locking on, block every subsequent run).
- SaaS — Laravel/Symfony scheduler crons dispatch queued jobs and run periodic reconciliation (failed-payment retries, subscription renewals, daily digests). Pair with a daemon running the queue worker.
- Tech agencies / large platforms — pin heavy nightly jobs (full-text index rebuilds, exports) to a specific 'Node' so they don't compete with HTTP workers on the main nodes.