Perfex CRM Cron Job Not Running: A Diagnosis Checklist
Perfex CRM cron job not working? The symptom is never the cron. Work backwards from missing invoices and silent reminders to the real cause, in ten minutes.
A ticket arrives saying task reminders have stopped. An hour later a client asks where their monthly invoice is. Someone in accounts mentions that overdue notices have gone quiet, and the support inbox has not pulled a new email since Thursday. Four features, four people reporting them, nothing in the application log connecting them.
They are one incident: a Perfex CRM cron job not working. Nothing on screen says so. Perfex does not raise an error when its background job stops, because from the application’s point of view nothing failed - the work was never requested. No retry, no red banner. The system goes quiet and stays quiet, and the first alarm is a customer.
A Perfex CRM cron job not working looks like five separate bugs
Perfex has a single background entry point: one PHP file under the install root, on a standard layout at crons/cron.php. That one file does not just handle invoices. Depending on your version, a single run is responsible for most of the following:
- Creating recurring invoices and recurring expenses
- Sending invoice overdue reminders and before-due reminders
- Estimate, proposal and contract expiry notifications
- Task reminders and deadline notices
- Ticket email import over IMAP, and ticket auto-close
- Customer and lead follow-up reminders
- The scheduled database backup, if you have it enabled
That is why the symptom never points at the cause. Nobody reports a dead cron; they report five unrelated features. If two or more of those break at once, stop investigating them individually. You have one problem, not several.
The corollary matters: if only one of those features is broken, it is almost certainly not the cron. Cron failure is all-or-nothing. Selective failure points elsewhere - a template, a permission, an SMTP transport - and the checks below will waste your afternoon.
Step 1: read the last cron run timestamp
Perfex records when the job last completed, and that number splits the problem in half before you touch a config file. Read it under Setup > Settings > Cron Job, or from the options table:
SELECT name, value FROM tbloptions WHERE name LIKE '%cron%';
If the value comes back as a bare integer it is a Unix timestamp - wrap it in FROM_UNIXTIME(value) to read it.
Now interpret it:
- Stale, hours or days old. The job is not being invoked at all. This is a scheduling or environment problem, and everything from step 2 onwards applies. Do not open an invoice yet.
- Current, within your interval. The job runs; something inside it is failing or switched off. Skip to the mail section, and check that feature’s own settings before blaming the schedule.
- Current but wrong by hours. Your CLI and web PHP disagree about the timezone. Harmless alone, but reminders fire at odd times and it hints the environments differ elsewhere.
Step 2: run it by hand from the shell
The highest-value thirty seconds in the whole process. Run the cron in the foreground and read what it prints:
php -q /home/user/public_html/crons/cron.php
echo "exit: $?"
Under a normal crontab entry the output goes to /dev/null and every error with it. Run it interactively and a fatal error, an exhausted memory limit, a missing extension or a database connection failure announces itself in plain text.
The result splits the diagnosis cleanly:
- It errors in the shell. Fix that error. The scheduler is fine; the code path is not.
- It completes in the shell, but the timestamp still never advances on schedule. The script is fine and the environment cron runs it in is not - the next section, and by far the most common case.
Run it as the system user that owns the web files; as root it works, then leaves root-owned files that break something a week later.
Step 3: the wrong PHP binary, and the rest of a broken Perfex CRM cron setup
Cron does not read your shell profile. It runs with a minimal PATH, usually /usr/bin:/bin, no ~/.bashrc, and none of your login environment. A crontab line saying php resolves against that minimal path and often finds a different binary from the one serving your site - an older system PHP with fewer extensions and a different php.ini.
Find the right one:
which php && php -v
readlink -f "$(which php)"
On cPanel with EasyApache the real binary is typically under /opt/cpanel/ea-php83/root/usr/bin/php; on Plesk, /opt/plesk/php/8.3/bin/php. Use the absolute path, always:
*/5 * * * * /opt/cpanel/ea-php83/root/usr/bin/php -q /home/user/public_html/crons/cron.php >> /home/user/logs/perfex-cron.log 2>&1
Two deliberate choices there. A log file instead of /dev/null turns the next incident into a two-minute read; add it to logrotate. Five minutes is the right interval - frequent enough that reminders leave near the hour you intended, rare enough that runs do not stack.
Prove which binary cron actually used
Do not assume. Add a throwaway entry for a minute:
* * * * * /usr/bin/env > /tmp/cronenv.txt; php -v >> /tmp/cronenv.txt 2>&1
Wait a minute, read /tmp/cronenv.txt, delete the entry. You now know the PATH cron handed you and which version a bare php resolves to there. That test explains most cases where the script runs by hand and never on schedule.
php-cli and php-fpm do not share a php.ini
Confirm which ini the CLI loads, then compare it against a phpinfo() page served over the web:
php -i | grep -E "Loaded Configuration File|max_execution_time|memory_limit|disable_functions"
Three settings decide whether a run finishes:
max_execution_time- the CLI default is unlimited, but plenty of shared hosts override it to 30. A run that dies at 30 seconds finishes the jobs early in the sequence and never reaches the rest, which produces the confusing symptom of some automation working and some not. Time a real run withtime php -q ...; within a factor of two of the limit is a bomb on a timer as the database grows.memory_limit- the same story with a fatal error instead of a silent stop.disable_functionsandopen_basedir- a disabled function, or a basedir that excludes your application path, stops the run before it starts. Both surface as a fatal in step 2, which is exactly why step 2 comes first.
Step 4: system cron, web cron or the Perfex cron URL
With no shell access, Perfex can be triggered over HTTP. Copy the cron URL from Setup > Settings > Cron Job rather than building it by hand - the path and key parameter have moved between versions. Point an external scheduler at it, or a curl call from a machine that has cron.
The three modes are not equivalent, and the differences bite in production:
| System cron | Web cron URL | External cron service | |
|---|---|---|---|
| PHP used | CLI binary, CLI ini | php-fpm, web ini | php-fpm, web ini |
| Time ceiling | Usually none | Web max_execution_time and fastcgi_read_timeout | Both, plus the service’s HTTP timeout |
| Failure visibility | Exit code and log file | A 504 in the access log, if anyone looks | Alert from the service |
| Needs shell access | Yes | No | No |
| Risk of double runs | Low, and flock removes it | Moderate | High - many services retry on timeout |
The killer for web cron is the timeout stack. Nginx defaults fastcgi_read_timeout to 60 seconds, so a 75-second run is cut off mid-sequence every time, and the only trace is a 504 in a log nobody reads. Worse, a service that treats the timeout as a failure retries, starting a second run while the first is still going.
Guard against overlap whichever mode you pick:
*/5 * * * * /usr/bin/flock -n /tmp/perfex-cron.lock /opt/cpanel/ea-php83/root/usr/bin/php -q /home/user/public_html/crons/cron.php >> /home/user/logs/perfex-cron.log 2>&1
flock -n exits immediately if the previous run still holds the lock. Overlapping runs are how you get duplicate invoices and double-sent reminders, and far easier to prevent than to clean up.
When the cron runs but Perfex CRM is not sending emails
A current timestamp with silent mail means the schedule is fine and something downstream is not. Separate the two layers before changing anything.
If the mail queue is enabled, the cron drains it, so a queue with no working schedule holds everything and reports nothing. If the queue is draining and mail still never lands, the fault is the transport, not the timing - test SMTP on its own, outside the CRM, before touching a crontab that is already correct.
One recovery detail catches people on the day they fix it. Reminders already marked as sent will not resend, so anything the dead period owed is gone; send those by hand or not at all. Recurring invoices behave differently: check the dates on whatever the first successful run creates, since one generated on the day the system noticed may not carry the date it was due.
Ten minutes, in this order
- Open
Setup > Settings > Cron Joband write down the last cron run timestamp. Stale means environment; current means feature. - Run
php -q /path/to/crons/cron.phpin the shell, as the web user, and read every line of output. - Run
readlink -f "$(which php)"and put that absolute path in the crontab. Never a barephp. - Add the one-minute
envprobe, confirm what cron actually resolves to, then delete it. - Compare CLI and web
max_execution_time, thentimea real run against the lower of the two. - Redirect cron output to a log file instead of
/dev/null, and wrap the command inflock -n. - Add a dead man’s switch: append
&& curl -fsS https://your-monitor/pingso the ping only fires on a clean exit, and alert if it goes quiet for fifteen minutes.
Step 7 is the one that still matters in six months. Everything above it fixes today’s outage; the ping turns the next one into a notification within the hour instead of a client asking where their invoice went.