Is Your WHMCS Cron Job Really Running? How to Debug Stuck and Overlapping Cron Processes

WHMCS cron job debug

The WHMCS cron job is one of those things that usually works quietly in the background — until it doesn’t.

Invoices, renewal reminders, domain synchronisation, automatic suspensions, ticket escalations and dozens of other automated tasks depend on it. For this reason, the standard WHMCS recommendation is to execute cron.php every few minutes.

For example:

*/5 * * * * /usr/local/bin/php -q /path/to/whmcs/crons/cron.php

But there is an important distinction:

a cron job being started every five minutes does not necessarily mean that it successfully completes every five minutes.

A process may start correctly and then hang somewhere during execution. Five minutes later, another process may be launched. Then another one.

Before long, instead of automation, you have a small collection of PHP processes patiently waiting for the end of the universe.

This article shows a simple way to find out what is actually happening.


The problem: “the cron is running” is not enough

When troubleshooting WHMCS automation, one of the first things we normally check is whether the system cron is configured correctly.

And it may indeed be configured correctly.

The operating system can faithfully execute:

php -q /path/to/whmcs/crons/cron.php

every five minutes.

That proves that the process starts.

It doesn’t prove that the previous process finished.

This distinction becomes particularly important when a WHMCS automation task hangs because of an external API, a network timeout, a problematic module, a database issue or some custom code.

What we really want to know is:

  • When did the WHMCS cron start?
  • When did it finish?
  • Which automation tasks were executed?
  • Which task started but never returned?
  • How long did the whole cron execution take?
  • Are multiple cron.php processes running simultaneously?

Fortunately, WHMCS hooks give us a convenient way to investigate.


Step 1: Log the beginning and end of the WHMCS cron

WHMCS provides two particularly useful hooks:

PreCronJob
AfterCronJob

PreCronJob is executed when the cron automation starts, while AfterCronJob is executed after the scheduled automation tasks have completed.

Create the following file:

/includes/hooks/cron_debug.php

and add:

<?php

if (!defined("WHMCS")) {
    die("This file cannot be accessed directly");
}


/**
 * WHMCS Cron Debug
 *
 * Logs the beginning and end of every cron execution.
 */


/**
 * Cron started
 */
add_hook('PreCronJob', 1, function ($vars) {

    logActivity(
        '[CRON DEBUG] >>> CRON START'
        . ' | PID: ' . getmypid()
        . ' | Time: ' . date('Y-m-d H:i:s')
        . ' | Memory: ' . round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
    );

});


/**
 * Cron completed
 */
add_hook('AfterCronJob', 1, function ($vars) {

    logActivity(
        '[CRON DEBUG] <<< CRON END'
        . ' | PID: ' . getmypid()
        . ' | Time: ' . date('Y-m-d H:i:s')
        . ' | Memory: ' . round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
    );

});

Now check the WHMCS Activity Log.

Under normal conditions, you should see something similar to:

[CRON DEBUG] >>> CRON START | PID: 17432 | Time: 2026-08-29 13:30:01 | Memory: 18 MB

[CRON DEBUG] <<< CRON END | PID: 17432 | Time: 2026-08-29 13:30:14 | Memory: 24 MB

Five minutes later:

[CRON DEBUG] >>> CRON START | PID: 17501 | Time: 2026-08-29 13:35:01 | Memory: 18 MB

[CRON DEBUG] <<< CRON END | PID: 17501 | Time: 2026-08-29 13:35:09 | Memory: 23 MB

Everything looks healthy.

But suppose you find:

13:30:01 >>> CRON START | PID: 17432
13:35:01 >>> CRON START | PID: 17501
13:40:01 >>> CRON START | PID: 17588

with no corresponding CRON END.

Now things become interesting.

Your scheduler is doing exactly what you asked it to do.

WHMCS, however, isn’t coming back.


Step 2: Find the WHMCS automation task that gets stuck

Knowing that cron.php hangs is useful.

Knowing where it hangs is much better.

WHMCS provides two additional hooks that can help:

PreAutomationTask
PostAutomationTask

We can therefore extend our debug hook:

<?php

if (!defined("WHMCS")) {
    die("This file cannot be accessed directly");
}


/**
 * WHMCS Cron Debug
 */


add_hook('PreCronJob', 1, function ($vars) {

    logActivity(
        '[CRON DEBUG] >>> CRON START'
        . ' | PID: ' . getmypid()
        . ' | Time: ' . date('Y-m-d H:i:s')
        . ' | Memory: ' . round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
    );

});


add_hook('PreAutomationTask', 1, function ($vars) {

    $task = $vars['task'] ?? null;
    $taskName = 'UNKNOWN';

    if (is_object($task)) {
        try {
            $taskName = method_exists($task, 'getName')
                ? $task->getName()
                : get_class($task);
        } catch (\Throwable $e) {
            $taskName = get_class($task);
        }
    }

    logActivity(
        '[CRON DEBUG] --> TASK START'
        . ' | Task: ' . $taskName
        . ' | PID: ' . getmypid()
        . ' | Time: ' . date('Y-m-d H:i:s')
        . ' | Memory: ' . round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
    );

});


add_hook('PostAutomationTask', 1, function ($vars) {

    $task = $vars['task'] ?? null;
    $completed = $vars['completed'] ?? null;
    $taskName = 'UNKNOWN';

    if (is_object($task)) {
        try {
            $taskName = method_exists($task, 'getName')
                ? $task->getName()
                : get_class($task);
        } catch (\Throwable $e) {
            $taskName = get_class($task);
        }
    }

    logActivity(
        '[CRON DEBUG] <-- TASK END'
        . ' | Task: ' . $taskName
        . ' | Completed: ' . var_export($completed, true)
        . ' | PID: ' . getmypid()
        . ' | Time: ' . date('Y-m-d H:i:s')
        . ' | Memory: ' . round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
    );

});


add_hook('AfterCronJob', 1, function ($vars) {

    logActivity(
        '[CRON DEBUG] <<< CRON END'
        . ' | PID: ' . getmypid()
        . ' | Time: ' . date('Y-m-d H:i:s')
        . ' | Memory: ' . round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
    );

});

Now the Activity Log becomes considerably more useful:

>>> CRON START

--> TASK START | Task: ProcessRenewals
<-- TASK END   | Task: ProcessRenewals

--> TASK START | Task: AutoSuspensions
<-- TASK END   | Task: AutoSuspensions

--> TASK START | Task: DomainSync

And then… nothing.

Five minutes later:

>>> CRON START

That’s a very good clue.

If DomainSync starts but its corresponding PostAutomationTask never appears, that’s the first place to investigate.

The problem may not necessarily be inside WHMCS itself. The task could be waiting for a registrar API, DNS service, payment gateway, provisioning module or another external resource.

But at least we now know where to start digging.

Which, in debugging terms, is approximately 90% of the battle.


Step 3: Don’t trust WHMCS alone — watch the process from outside

There is one limitation to the previous approach.

We’re using WHMCS to investigate WHMCS.

That’s useful, but philosophically questionable.

If PHP crashes, WHMCS fails very early during initialization, or execution gets stuck somewhere outside the hooks we’re monitoring, our logging may never get the opportunity to tell us what happened.

For this reason, we can add a second layer of monitoring outside WHMCS.

Create a shell script such as:

cron_debug.sh

with:

#!/bin/bash

LOG="/home/USER/cron_debug.log"

echo "========================================" >> "$LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') START cron - wrapper PID $$" >> "$LOG"

START=$(date +%s)

/usr/local/bin/php -q /path/to/whmcs/crons/cron.php >> "$LOG" 2>&1

EXITCODE=$?

END=$(date +%s)
DURATION=$((END - START))

echo "$(date '+%Y-%m-%d %H:%M:%S') END cron - exit=$EXITCODE duration=${DURATION}s" >> "$LOG"

Make it executable:

chmod +x cron_debug.sh

Then temporarily replace your normal cron command with:

*/5 * * * * /home/USER/cron_debug.sh

Now we’re monitoring the PHP process from the operating system’s point of view.

A normal execution could produce:

2026-08-29 13:30:01 START cron - wrapper PID 3345
2026-08-29 13:30:14 END cron - exit=0 duration=13s

2026-08-29 13:35:01 START cron - wrapper PID 3401
2026-08-29 13:35:11 END cron - exit=0 duration=10s

Much better.

We now know that PHP actually returned control to the shell.

But this:

13:30:01 START cron - wrapper PID 3345
13:30:14 END cron - exit=0 duration=13s

13:35:01 START cron - wrapper PID 3401

13:40:01 START cron - wrapper PID 3478

13:45:01 START cron - wrapper PID 3532

is rather less comforting.

The 13:35 process never returned.

And now additional cron processes are piling up behind it.


Step 4: Check for overlapping cron processes

On a Linux server, another extremely simple test is:

pgrep -af cron.php

Alternatively:

ps aux | grep cron.php

During a normal cron execution, you may temporarily see something like:

12345 php -q /home/user/whmcsdata/crons/cron.php

A few seconds later, it should disappear.

Finding this instead:

12345 php -q /home/user/whmcsdata/crons/cron.php
12401 php -q /home/user/whmcsdata/crons/cron.php
12488 php -q /home/user/whmcsdata/crons/cron.php
12542 php -q /home/user/whmcsdata/crons/cron.php

is a pretty strong indication that something isn’t behaving as expected.

Multiple cron processes can potentially cause all sorts of entertaining problems:

  • database contention;
  • API calls running concurrently;
  • duplicate operations;
  • delayed automation;
  • excessive memory consumption;
  • locks;
  • timeouts;
  • apparently random failures.

“Entertaining”, obviously, in the very specific sysadmin meaning of the word.


Step 5: Measure how long each WHMCS task takes

Once the basic debugging works, there’s another useful improvement: measuring the execution time of individual automation tasks.

This helps identify tasks that technically complete, but take an unexpectedly long time.

For example:

ProcessRenewals ............... 0.82 sec
AutoSuspensions ............... 1.14 sec
TicketEscalations ............. 0.31 sec
DomainSync ................... 94.72 sec
ProcessInvoicePayments ........ 0.65 sec

Nothing is technically “stuck” here.

But DomainSync certainly deserves a cup of coffee and some attention.

A simple implementation can store the start timestamp for each task and calculate the elapsed time when PostAutomationTask fires.

For temporary debugging, even basic timing information can be extremely valuable because it lets us distinguish between:

a task that never finishes and a task that finishes, but takes forever.

Those are two very different problems.


What can cause a WHMCS cron to hang?

Once you’ve identified the problematic task, the next question is why.

There is no single answer, but some common suspects are:

External APIs. Registrar modules, payment gateways, provisioning systems and other integrations may be waiting for a remote server that isn’t answering properly.

Network problems. DNS resolution failures, firewall issues, broken IPv6 connectivity or routing problems can make an HTTP request take far longer than expected.

PHP execution limits. Check max_execution_time, memory limits and CLI-specific PHP configuration. Remember that the PHP configuration used by CLI may be different from the one used by your web server.

Database problems. Slow queries, table locks or database resource exhaustion can turn an otherwise harmless automation task into a very long process.

Third-party WHMCS modules. Hooks and addon modules execute code inside WHMCS. A badly behaved integration can therefore affect the entire automation process.

Your own hooks. Yes, unfortunately.

That beautiful 300-line hook you wrote at 2 AM because “it’s just a quick workaround” is now part of the suspect list.

Welcome to software development.


Cron frequency and task frequency are not the same thing

There is another important point that sometimes causes confusion.

Running:

*/5 * * * *

doesn’t mean that every WHMCS automation task runs every five minutes.

It means that WHMCS automation is invoked every five minutes.

WHMCS then determines which scheduled tasks are due to run during that particular execution.

This is important when debugging intermittent problems.

If the cron normally completes in ten seconds but mysteriously hangs once per day, don’t immediately blame the five-minute cron schedule.

Look at which automation task runs only during the problematic execution.

That’s often where the interesting part begins.


A useful debugging strategy

When investigating a suspicious WHMCS cron, we normally want visibility at three different levels:

Operating system
      ↓
PHP cron.php process
      ↓
WHMCS automation tasks

The shell wrapper tells us whether the PHP process actually starts and terminates.

PreCronJob and AfterCronJob tell us whether WHMCS automation enters and exits normally.

PreAutomationTask and PostAutomationTask tell us what happens inside the automation cycle.

Put the three together and a vague report such as:

“Sometimes the WHMCS cron seems to get stuck.”

can become something considerably more useful:

13:35:01 cron.php started
13:35:03 DomainSync started
13:40:01 another cron.php process started
13:45:01 another cron.php process started

DomainSync never returned.

Now we have something we can actually debug.


Final thoughts

Cron problems are particularly annoying because automation is supposed to be invisible.

When everything works, nobody thinks about it.

When something fails, the first symptom may appear hours later as a missing invoice, an unsent renewal reminder or an automation task that mysteriously hasn’t executed.

For this reason, when debugging WHMCS automation, don’t ask only:

“Did cron start?”

Ask:

“Did it finish?”

And then:

“What exactly happened between those two events?”

Computers are remarkably literal creatures.

If you ask them the right questions — and log the answers — they usually confess eventually.

Leave a Reply 0

Your email address will not be published. Required fields are marked *