Skip to content

Scheduled Tasks Run Every Minute Due to Incorrect Timezone in getNextRunDate #5380

@Fallen-Breath

Description

@Fallen-Breath

Description

After performing maintenance on the operating system hosting Pterodactyl, I encountered an issue where all scheduled tasks are executing every minute, regardless of their configured cron expressions

I searched for related issues in this repository and found #3351 and #4536, but neither provided a viable solution. Therefore, I decided to investigate the issue myself

Environment

  • System Timezone: UTC+8, Asia/Shanghai

    # timedatectl
                   Local time: Mon 2025-07-07 02:27:34 CST
               Universal time: Sun 2025-07-06 18:27:34 UTC
                     RTC time: Sun 2025-07-06 18:27:34
                    Time zone: Asia/Shanghai (CST, +0800)
    System clock synchronized: yes
                  NTP service: active
              RTC in local TZ: no
    
  • Panel Version: 1.11.11.0, installed directly on the system (note: I'm using a fork from https://github.com/pterodactyl-china/panel, but my analysis indicates this issue is unrelated to the fork)

  • Wings Version: 1.11.14.0 (though this issue is unrelated to Wings)

  • Timezone Configuration: All relevant configurations in the system and Pterodactyl panel explicitly set the timezone to Asia/Shanghai, including .env with APP_TIMEZONE=Asia/Shanghai

Analysis

I first checked the database and found anomalies in the next_run_at field of scheduled tasks:

MariaDB [panel]> select last_run_at, next_run_at from schedules;
+---------------------+---------------------+
| last_run_at         | next_run_at         |
+---------------------+---------------------+
| 2025-07-07 02:33:02 | 2025-07-06 18:40:00 |   // crontab expr: 0,20,40 * * * *
| 2025-07-07 02:33:03 | 2025-07-06 18:50:00 |   // crontab expr: 10,30,50 * * * *
+---------------------+---------------------+
2 rows in set (0.000 sec)

This query was executed at 2025-07-07 02:33 UTC+8. Notably, the next_run_at values are earlier than last_run_at. Additionally, next_run_at is approximately 8 hours behind the current time, suggesting that next_run_at is being set using the UTC+0 timezone instead of the local timezone (UTC+8, Asia/Shanghai)

Then I looked into the code. Note that I am not familiar with PHP, so please point out any oversights in my analysis

The next_run_at field is updated after a scheduled task executes, in the following code:

$this->connection->transaction(function () use ($schedule, $task) {
$schedule->forceFill([
'is_processing' => true,
'next_run_at' => $schedule->getNextRunDate(),
])->saveOrFail();

The getNextRunDate function is implemented as follows:

public function getNextRunDate(): CarbonImmutable
{
$formatted = sprintf('%s %s %s %s %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_month, $this->cron_day_of_week);
return CarbonImmutable::createFromTimestamp(
(new CronExpression($formatted))->getNextRunDate()->getTimestamp()
);
}

Here lies the bug: the CarbonImmutable::createFromTimestamp function always returns a date in the UTC+0 timezone, ignoring the system's or configured timezone (Asia/Shanghai):

https://github.com/briannesbitt/Carbon/blob/fb1ce5baa773517aa8e5a9a599179cf5fd8dc189/src/Carbon/Traits/Timestamp.php#L23-L36

As a result, getNextRunDate sets next_run_at to a date string in UTC+0. When the system or configured timezone is not UTC+0, the scheduling issue arises:

$schedules = Schedule::query()
->with('tasks')
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
->where('is_active', true)
->where('is_processing', false)
->whereRaw('next_run_at <= NOW()')

Possible Fix

Modify the Schedule::getNextRunDate implementation by replacing:

return CarbonImmutable::createFromTimestamp(
    (new CronExpression($formatted))->getNextRunDate()->getTimestamp()
);

with:

return CarbonImmutable::instance(
    (new CronExpression($formatted))->getNextRunDate()
);

This change ensures that getNextRunDate inherits the timezone from CronExpression->getNextRunDate(), avoiding the UTC+0 issue

Additional Information

A similar function in the codebase does not exhibit this issue:

public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
{
return Carbon::instance((new CronExpression(
sprintf('%s %s %s %s %s', $minute, $hour, $dayOfMonth, $month, $dayOfWeek)
))->getNextRunDate());
}

This function uses Carbon::instance, which correctly preserves the timezone of the DateTime object returned by CronExpression.

Further confirmation

To confirm, I added debugging code in ProcessScheduleService.php after line 41 (reference):

$debugInfo = [
    'Timestamp: ' . now()->toDateTimeString(),
    'Laravel Timezone: ' . config('app.timezone'),
    'PHP Default Timezone: ' . date_default_timezone_get(),
    'Current Time: ' . now()->toDateTimeString(),
];

$cronExpression = '0,20,40 * * * *';

$first = CarbonImmutable::createFromTimestamp((new CronExpression($cronExpression))->getNextRunDate()->getTimestamp());
$debugInfo[] = 'getNextRunDate: ' . $first->toDateTimeString() . ' (' . $first->getTimezone()->getName() . ')';

$second = Carbon::instance((new CronExpression($cronExpression))->getNextRunDate());
$debugInfo[] = 'getScheduleNextRunDate: ' . $second->toDateTimeString() . ' (' . $second->getTimezone()->getName() . ')';

$third = CarbonImmutable::instance((new CronExpression($cronExpression))->getNextRunDate());
$debugInfo[] = 'getNextRunDate (fixed): ' . $third->toDateTimeString() . ' (' . $third->getTimezone()->getName() . ')';

file_put_contents('/tmp/schedule_timezone_debug.log', implode(PHP_EOL, $debugInfo) . PHP_EOL . str_repeat('-', 50) . PHP_EOL, FILE_APPEND);

After running php artisan down && php artisan queue:restart && php artisan up, the /tmp/schedule_timezone_debug.log file showed:

Timestamp: 2025-07-07 02:52:01
Laravel Timezone: Asia/Shanghai
PHP Default Timezone: Asia/Shanghai
Current Time: 2025-07-07 02:52:01
getNextRunDate: 2025-07-06 19:00:00 (+00:00)
getScheduleNextRunDate: 2025-07-07 03:00:00 (Asia/Shanghai)
getNextRunDate (fixed): 2025-07-07 03:00:00 (Asia/Shanghai)
--------------------------------------------------

which further supports my analysis

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions