walkor/workerman · error · RuntimeException

Timer can only be used in workerman running environment

Error message

Timer can only be used in workerman running environment

What it means

Timer::add() needs either an installed event loop (set via Timer::init by a running Worker) or Worker instances registered in the process. When both are absent - i.e. the code is executing outside a Workerman runtime - it cannot schedule anything and throws instead of pretending to work.

Source

Thrown at src/Timer.php:157

     * @return int
     */
    public static function add(float $timeInterval, callable $func, ?array $args = [], bool $persistent = true): int
    {
        if ($timeInterval < 0) {
            throw new RuntimeException('$timeInterval can not less than 0');
        }

        if ($args === null) {
            $args = [];
        }

        if (self::$event) {
            return $persistent ? self::$event->repeat($timeInterval, $func, $args) : self::$event->delay($timeInterval, $func, $args);
        }

        // If not workerman runtime just return.
        if (!Worker::getAllWorkers()) {
            throw new RuntimeException('Timer can only be used in workerman running environment');
        }

        if (empty(self::$tasks)) {
            pcntl_alarm(1);
        }

        $runTime = (int)floor(time() + $timeInterval);
        if (!isset(self::$tasks[$runTime])) {
            self::$tasks[$runTime] = [];
        }

        self::$timerId = self::$timerId == PHP_INT_MAX ? 1 : ++self::$timerId;
        self::$status[self::$timerId] = true;
        self::$tasks[$runTime][self::$timerId] = [$func, (array)$args, $persistent, $timeInterval];

        return self::$timerId;
    }

View on GitHub (pinned to 1391112a61)

Solutions

  1. Move Timer::add() calls into worker lifecycle callbacks, typically onWorkerStart, where the event loop exists
  2. If you need a timer outside Workerman, use a different mechanism (pcntl_alarm, usleep loop, sched/cron) guarded by an environment check
  3. For tests, initialize the loop explicitly (Timer::init(new Workerman\Events\Select())) or mock the Timer calls

Example fix

// before
Timer::add(1, fn() => echo 'tick'); // top of script, no worker running -> throws
$worker = new Worker('http://0.0.0.0:8080');
Worker::runAll();

// after
$worker = new Worker('http://0.0.0.0:8080');
$worker->onWorkerStart = function () {
    Timer::add(1, fn() => echo 'tick'); // loop is ready here
};
Worker::runAll();
Defensive patterns

Strategy: validation

Validate before calling

if (!Timer::initialized() && !Worker::getAllWorkers()) { // guard before scheduling
    error_log('Timer skipped: not running under Workerman');
    return;
}
Timer::add(1, $cb);

Prevention

When it happens

Trigger: Calling Timer::add() in a plain CLI script or unit test that never starts workers; calling it in bootstrap code before Worker::runAll()/Worker::run() has initialized the event loop; a shared library file used both by Workerman workers and standalone cron scripts.

Common situations: Running part of the application via 'php bin/job.php' for debugging; PHPUnit bootstraps that touch code paths using Timer; composer post-install scripts reusing app code; calling Timer::add at file top-level instead of inside a worker callback.

Related errors


AI-assisted analysis of walkor/workerman@1391112a61 (2026-08-21). Data as JSON: /api/errors/5773024d6ee79cb7. Report an issue: GitHub.