A native PHP extension that brings a high-performance event loop directly into the engine. Inspired by and API-compatible with Revolt -- not a replacement, but a native alternative written in C for zero-overhead async I/O.
Why? Revolt is an excellent userland library. This extension takes the same proven API design and moves it into a PHP extension, eliminating userland dispatch overhead and leveraging OS-level I/O primitives (epoll, kqueue, poll) directly from C.
| Revolt | ext-eventloop | |
|---|---|---|
| Implementation | PHP userland | C extension |
| Installation | composer require revolt/event-loop |
phpize && make install |
| I/O backend | Configurable (ev, event, uv) | Auto-detected (epoll / kqueue / poll / select) |
| Fiber suspension | Yes | Yes |
| API contract | Revolt\EventLoop::* |
EventLoop\EventLoop::* |
The API surface mirrors Revolt's, so migrating between the two is straightforward -- adjust the namespace and you're done.
- PHP >= 8.1 (Fiber support required)
- A POSIX-compatible OS (Linux, macOS, FreeBSD, etc.)
pie install axcherednikov/eventloopgit clone https://github.com/axcherednikov/php-eventloop.git
cd php-eventloop
phpize
./configure --enable-eventloop
make
make test
sudo make installThen enable the extension:
; php.ini or conf.d/eventloop.ini
extension=eventloopVerify:
php -m | grep eventloop<?php
use EventLoop\EventLoop;
// Defer a callback to the next loop tick
EventLoop::defer(function (string $callbackId) {
echo "Deferred callback executed\n";
});
// Delay execution by 1.5 seconds
EventLoop::delay(1.5, function (string $callbackId) {
echo "This runs after 1.5 seconds\n";
});
// Repeat every 500ms
$id = EventLoop::repeat(0.5, function (string $callbackId) {
echo "Tick\n";
});
// Cancel the repeater after 3 seconds
EventLoop::delay(3, function () use ($id) {
EventLoop::cancel($id);
});
EventLoop::run();All methods are static on EventLoop\EventLoop.
| Method | Description |
|---|---|
queue(Closure $closure, mixed ...$args): void |
Queue a microtask for immediate execution |
defer(Closure $closure): string |
Defer to the next event loop iteration |
delay(float $delay, Closure $closure): string |
Execute after $delay seconds |
repeat(float $interval, Closure $closure): string |
Execute every $interval seconds |
| Method | Description |
|---|---|
onReadable(resource $stream, Closure $closure): string |
Execute when a stream becomes readable |
onWritable(resource $stream, Closure $closure): string |
Execute when a stream becomes writable |
| Method | Description |
|---|---|
onSignal(int $signal, Closure $closure): string |
Execute when a signal is received |
| Method | Description |
|---|---|
enable(string $id): string |
Enable a disabled callback |
disable(string $id): string |
Disable a callback (can be re-enabled) |
cancel(string $id): void |
Permanently cancel a callback |
reference(string $id): string |
Reference a callback (keeps the loop alive) |
unreference(string $id): string |
Unreference a callback |
isEnabled(string $id): bool |
Check if a callback is enabled |
isReferenced(string $id): bool |
Check if a callback is referenced |
getType(string $id): CallbackType |
Get the callback type |
getIdentifiers(): array |
Get all registered callback IDs |
| Method | Description |
|---|---|
run(): void |
Run the event loop |
stop(): void |
Stop the event loop |
isRunning(): bool |
Check if the loop is running |
getDriver(): string |
Get the active I/O driver name |
| Method | Description |
|---|---|
setErrorHandler(?Closure $handler): void |
Set the error handler for exceptions in callbacks |
getErrorHandler(): ?Closure |
Get the current error handler |
$fiber = new Fiber(function () {
$suspension = EventLoop::getSuspension();
EventLoop::defer(function () use ($suspension) {
$suspension->resume('hello');
});
$value = $suspension->suspend(); // "hello"
echo $value; // "hello"
});
$fiber->start();
EventLoop::run();| Method | Description |
|---|---|
Suspension::suspend(): mixed |
Suspend the current fiber |
Suspension::resume(mixed $value = null): void |
Resume with a value |
Suspension::throw(Throwable $e): void |
Resume by throwing an exception |
The extension automatically selects the best I/O driver available on your system at compile time. There is no manual configuration needed -- you always get optimal performance for your platform.
| Driver | Platforms | Scalability | Notes |
|---|---|---|---|
| epoll | Linux 2.6+ | O(1) | Kernel tracks descriptors; returns only ready ones |
| kqueue | macOS, FreeBSD, OpenBSD | O(1) | Same principle as epoll, native to BSD systems |
| poll | Any POSIX | O(n) | No descriptor limit, but scans all on every call |
| select | Universal (fallback) | O(n) | Oldest API, limited to ~1024 descriptors |
Selection priority: epoll > kqueue > poll > select. The first one that compiles and initializes successfully wins.
In practice this means:
- Linux servers (the most common deployment) get epoll -- handles thousands of connections with near-zero overhead
- macOS (local development) gets kqueue -- equally efficient
- Older or exotic systems gracefully fall back to poll or select
Check which driver is active:
echo EventLoop::getDriver(); // "epoll" on Linux, "kqueue" on macOSEnvironment: PHP 8.5.4, Apple M1 Max, macOS, 100,000 iterations. Revolt v1.0.8 with StreamSelectDriver (default, no ext-ev/ext-uv). ext-eventloop using kqueue driver.
| Benchmark | Revolt | ext-eventloop | Speedup |
|---|---|---|---|
defer() dispatch |
715,053 ops/sec | 3,712,263 ops/sec | 5.2x |
delay(0) dispatch |
234,229 ops/sec | 2,832,500 ops/sec | 12.1x |
repeat() dispatch |
679,452 ops/sec | 17,794,516 ops/sec | 26.2x |
| I/O register + cancel | 2,109,267 ops/sec | 7,635,677 ops/sec | 3.6x |
| Fiber suspend/resume | 221,776 ops/sec | 248,738 ops/sec | 1.1x |
Note: Revolt was tested with its default StreamSelectDriver. With ext-ev or ext-uv backends, Revolt's I/O performance would be higher, though callback dispatch overhead remains in PHP userland. Fiber performance is nearly identical because
suspend()/resume()is handled by the Zend Engine in both cases.
The API contract is intentionally compatible. In most cases, a namespace swap is all you need:
- use Revolt\EventLoop;
+ use EventLoop\EventLoop;If you use Revolt\EventLoop\Suspension:
- use Revolt\EventLoop\Suspension;
+ use EventLoop\Suspension;make testThe extension ships with 26 .phpt tests covering defer, delay, repeat, I/O watchers, signals, suspensions, error handling, and edge cases.
This project is built on the ideas and API design of Revolt by Aaron Piotrowski, Niklas Keller, and contributors. Revolt's clean, well-thought-out API made it the natural foundation for a native implementation. Full credit to the Revolt team for defining the contract that this extension follows.
Licensed under the MIT License.