← Blog
Perfex CRMSelf-HostingTroubleshooting

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 with time 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_functions and open_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 cronWeb cron URLExternal cron service
PHP usedCLI binary, CLI iniphp-fpm, web iniphp-fpm, web ini
Time ceilingUsually noneWeb max_execution_time and fastcgi_read_timeoutBoth, plus the service’s HTTP timeout
Failure visibilityExit code and log fileA 504 in the access log, if anyone looksAlert from the service
Needs shell accessYesNoNo
Risk of double runsLow, and flock removes itModerateHigh - 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

  1. Open Setup > Settings > Cron Job and write down the last cron run timestamp. Stale means environment; current means feature.
  2. Run php -q /path/to/crons/cron.php in the shell, as the web user, and read every line of output.
  3. Run readlink -f "$(which php)" and put that absolute path in the crontab. Never a bare php.
  4. Add the one-minute env probe, confirm what cron actually resolves to, then delete it.
  5. Compare CLI and web max_execution_time, then time a real run against the lower of the two.
  6. Redirect cron output to a log file instead of /dev/null, and wrap the command in flock -n.
  7. Add a dead man’s switch: append && curl -fsS https://your-monitor/ping so 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.