Timers

Server-level recurring tasks via OpenSwoole\Timer. Each worker runs its own timers — use App::onWorkerStart() to register them.

Recurring timer in every worker
// In app.php (before run()):
$tickCounter = new Counter(0);

App::onWorkerStart(function($server, $workerId) use ($tickCounter) {
    // Starts 1 timer per worker; N workers = N timers, all incrementing
    App::tick(2000, function() use ($tickCounter) {
        $tickCounter->increment();
    });
});

// One-shot timer from a route:
$app->route('/timers/oneshot', function($response) {
    $response->stream(function($write) {
        $result = new Channel(1);
        App::after(3000, fn() => $result->push('done after 3s'));
        $write($result->pop(5));
    });
});
SSE — /timers/sse
$app->route('/timers/sse', function($response) use ($tickCounter, $requestCounter) {
    $requestCounter->increment();
    $response->sse(function($emit) use ($tickCounter, $requestCounter) {
        $emit(json_encode(['event' => 'connected', 'tick' => $tickCounter->get()]), 'open');
        for ($i = 0; $i < 20; $i++) {
            usleep(2000000);
            $emit(json_encode([
                'tick'     => $tickCounter->get(),
                'requests' => $requestCounter->get(),
                'time'     => date('H:i:s'),
            ]), 'tick', (string)$i);
        }
        $emit(json_encode(['done' => true]), 'done');
    });
});
GET Counter incremented by tick timers
// tick_count = total increments across all workers and 2s intervals
App::onWorkerStart(function($server, $workerId) use ($tickCounter) {
    App::tick(2000, fn() => $tickCounter->increment());
});
$app->route('/timers/counter', function() use ($requestCounter, $tickCounter) {
    $requestCounter->increment();
    return ['requests_served' => $requestCounter->get(), 'tick_count' => $tickCounter->get()];
});
LIVE OUTPUT Click Run →
GET One-shot delayed task
$app->route('/timers/oneshot', function($response) use ($requestCounter) {
    $requestCounter->increment();
    $response->stream(function($write) {
        $result = new Channel(1);
        App::after(3000, function() use ($result) {
            $result->push(['done' => true, 'time' => date('H:i:s'), 'pid' => getmypid()]);
        });
        $write($result->pop(5));
    });
});
LIVE OUTPUT Click Run →
GET Per-worker metrics via Store
Store::make('worker_metrics', 64, [
    'pid'      => [\OpenSwoole\Table::TYPE_INT, 4],
    'ticks'    => [\OpenSwoole\Table::TYPE_INT, 8],
]);
App::onWorkerStart(function($server, $workerId) use ($tickCounter) {
    $pid = getmypid();
    Store::set('worker_metrics', (string)$workerId, ['pid' => $pid, 'ticks' => 0]);
    App::tick(2000, function() use ($workerId, $tickCounter) {
        $tickCounter->increment();
        Store::incr('worker_metrics', (string)$workerId, 'ticks');
    });
});
LIVE OUTPUT Click Run →
SSE/timers/sse — Server-Sent Events
// Connect with EventSource and stream tick events.
const es = new EventSource('/timers/sse');
es.addEventListener('tick', e => console.log(JSON.parse(e.data)));
es.addEventListener('done', () => es.close());
LIVE OUTPUT
Click Connect to start…

Timer API

MethodWhen to use
App::tick(int $ms, callable $fn)Recurring task — runs every $ms milliseconds in this worker
App::after(int $ms, callable $fn)One-shot — fires once after $ms milliseconds
App::clearTimer(int $id)Cancel a tick/after timer by its returned ID
App::onWorkerStart(callable $fn)Register a callback called when each worker starts — right place for timers
Must be called inside a coroutine context. App::tick() works inside onWorkerStart callbacks and route handlers, but not at the global PHP scope (before the server starts).