walkor/workerman · error · RuntimeException

Event::addTimer($delay) failed

Error message

Event::addTimer($delay) failed

What it means

This is Workerman's libevent (ext-event) event-loop adapter. delay() schedules one-shot timers by constructing an \Event with TIMEOUT and calling addTimer($delay); libevent returns false when the timer cannot be armed — in practice when the delay is negative (or the event base is broken). The adapter turns that boolean into a RuntimeException so failures are not silently dropped callbacks.

Source

Thrown at src/Events/Event.php:109

        } else {
            $className = '\EventBase';
        }
        $this->eventBase = new $className();
    }

    /**
     * {@inheritdoc}
     */
    public function delay(float $delay, callable $func, array $args = []): int
    {
        $className = $this->eventClassName;
        $timerId = $this->timerId++;
        $event = new $className($this->eventBase, -1, $className::TIMEOUT, function () use ($func, $args, $timerId) {
            unset($this->eventTimer[$timerId]);
            $this->safeCall($func, $args);
        });
        if (!$event->addTimer($delay)) {
            throw new \RuntimeException("Event::addTimer($delay) failed");
        }
        $this->eventTimer[$timerId] = $event;
        return $timerId;
    }

    /**
     * {@inheritdoc}
     */
    public function offDelay(int $timerId): bool
    {
        if (isset($this->eventTimer[$timerId])) {
            $this->eventTimer[$timerId]->del();
            unset($this->eventTimer[$timerId]);
            return true;
        }
        return false;
    }

View on GitHub (pinned to 1391112a61)

Solutions

  1. Clamp the interval before scheduling: `$delay = max(0.001, $delay); Timer::add($delay, $fn, [], false);`.
  2. Find where the negative value originates (deadline math like $deadline - microtime(true)) and guard it at the source.
  3. If every timer fails even with positive delays, verify the ext-event installation (php --ri event) or force a different loop, e.g. `Worker::$eventLoopClass = Workerman\Events\Ev::class;` (or revolt/Fiber).

Example fix

// before: $delay goes negative once the deadline has passed
Timer::add($deadline - microtime(true), $fn, [], false);
// RuntimeException: Event::addTimer(-2.5) failed

// after: clamp to a minimal positive interval
Timer::add(max(0.001, $deadline - microtime(true)), $fn, [], false);
Defensive patterns

Strategy: validation

Validate before calling

function scheduleOnce(float $delay, callable $fn): int
{
    if ($delay <= 0) {
        $delay = 0.001; // or log and skip
    }
    return Workerman\Timer::add($delay, $fn, [], false);
}

Type guard

function isSchedulableDelay(mixed $delay): bool
{
    return is_numeric($delay) && (float)$delay > 0;
}

Try / catch

try {
    Timer::add($delay, $fn, [], false);
} catch (RuntimeException $e) {
    // libevent refused the timer; degrade to immediate execution
    $fn();
}

Prevention

When it happens

Trigger: Workerman\Timer::add($t, $fn, $args, false) (persistent=false maps to delay()) with $t <= 0 — typically a negative interval computed from a deadline ('now - startTime' after the deadline passed), or the event extension being misconfigured such that every addTimer fails. Fires only when the loop in use is the Event (libevent) implementation.

Common situations: Retry/backoff code computing 'next attempt time - now' that goes negative under load; passing -1 as a 'no delay' sentinel; crontab-style schedules that compute a negative offset; environments where ext-event is installed (so it is auto-selected) but its event base is unavailable.

Related errors


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