PHP 8.6 ships a Polling API in core. For the first time, plain PHP can wait on thousands of sockets with epoll or kqueue without an extension. This post explains what the API does, what it deliberately leaves out, and how it relates to the reactor that has powered OpenSwoole servers for years.
Until PHP 8.6, the only way to wait for I/O readiness in core PHP was stream_select(). The Polling API RFC lists its problems:
select() has a maximum file descriptor limit, typically 1024 on many systemsThe last point is the main motivation. The RFC is first of all an internal API: it is meant to improve signal handling in thread-safe builds, event handling in PHP-FPM, and timers across platforms. The userland API is the secondary benefit, and it is the part most of us will touch. The RFC was accepted with 33 votes in favour, 1 against and 4 abstentions.
Everything lives in the Io\Poll namespace:
Context is the poller. You add handles to it and call wait().StreamPollHandle wraps a stream resource so it can be polled.Watcher is returned by Context::add(). It tells you which events fired, carries your own data, and lets you modify or remove the registration.Event is an enum: Read, Write, Error, HangUp, ReadHangUp, OneShot and EdgeTriggered.Backend is an enum: Auto, Poll, Epoll, Kqueue, EventPorts and WSAPoll.The backend is selected automatically: epoll on Linux, kqueue on BSD and macOS, event ports on Solaris and illumos, WSAPoll on Windows, and poll() as the fallback on other POSIX systems. Error and HangUp are always monitored, and edge-triggered mode is available with epoll and kqueue only.
Here is a small TCP echo server written against the API described in the RFC:
<?php
use Io\Poll\{Context, Event};
$server = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr);
stream_set_blocking($server, false);
$poll = new Context();
$poll->add(new StreamPollHandle($server), [Event::Read], 'server');
while (true) {
foreach ($poll->wait(1) as $watcher) {
if ($watcher->getData() === 'server') {
$client = stream_socket_accept($server, 0);
if ($client) {
stream_set_blocking($client, false);
$poll->add(new StreamPollHandle($client), [Event::Read], 'client');
}
continue;
}
$client = $watcher->getHandle()->getStream();
if ($watcher->hasTriggered(Event::HangUp) || $watcher->hasTriggered(Event::Error)) {
$watcher->remove();
fclose($client);
continue;
}
$data = fread($client, 8192);
if ($data === '' || $data === false) {
$watcher->remove();
fclose($client);
continue;
}
fwrite($client, $data);
}
}
wait() takes a timeout in seconds, an optional microseconds part and an optional maximum number of events, and returns the watchers that have something to report.
This is a real improvement over stream_select(): no array juggling, no 1024 limit, and epoll or kqueue underneath. But notice what the example still has to do by hand.
The RFC is explicit that the userland API "provides polling capabilities, not a complete event loop". In PHP 8.6 that means:
fwrite() in the example can write only part of the data on a busy socket. Handling that correctly means keeping a buffer per connection and watching for Event::Write.PDO, mysqli, curl_exec(), file_get_contents() and sleep() still block the whole process.None of this is a criticism. The Polling API is meant to be the layer that event loop libraries such as Revolt, AMPHP and ReactPHP can build on, so they no longer need stream_select() or an optional extension to scale.
The OpenSwoole reactor does the same job as Io\Poll\Context, but it sits underneath a complete server:
<?php
$server = new OpenSwoole\Server('0.0.0.0', 8080);
$server->set(['worker_num' => 4]);
$server->on('receive', function ($server, int $fd, int $reactorId, string $data) {
$server->send($fd, $data);
});
$server->start();
Accepting connections, buffering writes, detecting closed connections, distributing connections across worker processes and restarting crashed workers are all handled for you. And with coroutine hooks enabled, blocking PHP code inside the callback yields to the reactor instead of blocking the worker:
<?php
OpenSwoole\Runtime::enableCoroutine(true, OpenSwoole\Runtime::HOOK_ALL);
$server = new OpenSwoole\Http\Server('0.0.0.0', 9501);
$server->on('request', function ($request, $response) {
// PDO, cURL and sleep() become non-blocking inside a coroutine
$pdo = new PDO('mysql:host=127.0.0.1;dbname=app', 'user', 'secret');
$rows = $pdo->query('SELECT id, title FROM posts LIMIT 10')->fetchAll();
$response->header('Content-Type', 'application/json');
$response->end(json_encode($rows));
});
$server->start();
Since OpenSwoole 26.2.0 you can also choose the reactor backend at runtime, including io_uring on Linux:
Co::set(['reactor_type' => OPENSWOOLE_IO_URING]);
See the reactor type and io_uring documentation for the available backends and build requirements.
| PHP 8.6 Polling API | OpenSwoole | |
|---|---|---|
| Availability | PHP core, no extension | PHP extension |
| Polling backends | epoll, kqueue, event ports, WSAPoll, poll | epoll, kqueue, poll, select, io_uring |
| What can be polled | Streams | Sockets, streams and pipes |
| Event loop | Not included | Built in |
| Timers | Not included | OpenSwoole\Timer |
| Coroutines | Not included | Built in |
| Blocking PHP functions | Still block | Made non-blocking by runtime hooks |
| Async file I/O | Not included | Thread pool or io_uring |
| Multi-process workers | Not included | Built in |
| Protocol servers | Build your own | TCP, UDP, HTTP, HTTP/2, WebSocket, gRPC |
Use the Polling API when you are writing a library or tool that has to run on any stock PHP 8.6 install: an event loop driver, a small proxy, a CLI tool that talks to many sockets at once, or a test harness. It removes the biggest scaling limit from pure PHP networking code.
Use OpenSwoole when you are building an application server: an HTTP API, a WebSocket or gRPC service, or anything that has to combine many connections with database calls, HTTP clients, timers and multiple CPU cores. That is the part the Polling API intentionally does not cover.
The two are not in competition. A native polling primitive in core is good news for everyone doing event-driven PHP, and it brings core PHP one step closer to the model OpenSwoole applications already use: one long-running process, many connections, and no thread or process per request.
Join 4,000+ others and never miss out on new tips, tutorials, and more.