How PHP’s New Poll API Boosts I/O Performance with Epoll, Kqueue, and More
The proposal introduces a unified poll API for PHP that replaces the limited stream_select() function with a flexible, platform‑aware interface supporting epoll, kqueue, WSAPoll and other backends, enabling high‑performance, scalable I/O handling for servers and event‑driven applications.
Introduction
PHP currently provides only stream_select() for I/O multiplexing. It is based on the select() system call and suffers from limited scalability (typically a 1024‑fd limit), poor performance with many descriptors, and no access to modern high‑performance mechanisms such as epoll (Linux) or kqueue (BSD/macOS). Modern servers that handle thousands of concurrent connections would benefit from a unified, high‑performance poll API.
Proposal
The RFC adds a new poll API to the PHP core. The API automatically selects the best available backend on the current platform while allowing developers to override the choice for testing or compatibility.
Supported Backends
epoll – Linux
kqueue – BSD, macOS
event ports – Solaris, illumos
WSAPoll – Windows
poll – fallback for other POSIX systems
PollHandle Abstraction
The core abstraction is the PollHandle abstract class, representing any pollable resource. The initial implementation provides StreamPollHandle for stream resources. Future extensions will add SocketPollHandle and CurlPollHandle to support socket extensions and libcurl descriptors.
API Overview
<?php
/** Event flags for readability monitoring */
const POLL_EVENT_READ = UNKNOWN;
/** Event flags for writability monitoring */
const POLL_EVENT_WRITE = UNKNOWN;
/** Event flag for error conditions (output only) */
const POLL_EVENT_ERROR = UNKNOWN;
/** Event flag for hang‑up conditions (output only) */
const POLL_EVENT_HUP = UNKNOWN;
/** Remote half‑close flag (Linux epoll only) */
const POLL_EVENT_RDHUP = UNKNOWN;
/** One‑shot mode – automatically removes the watcher after a single trigger */
const POLL_EVENT_ONESHOT = UNKNOWN;
/** Edge‑triggered mode (epoll/kqueue only) */
const POLL_EVENT_ET = UNKNOWN;
enum PollBackend : string {
case Auto = "auto";
case Poll = "poll";
case Epoll = "epoll";
case Kqueue = "kqueue";
case EventPorts = "eventport";
case WSAPoll = "wsapoll";
}
/** Abstract base class for pollable handles */
abstract class PollHandle {
/** Get the underlying file descriptor */
protected function getFileDescriptor(): int {}
}
/** Poll handle for stream resources */
final class StreamPollHandle extends PollHandle {
public function __construct($stream) {}
public function getStream() {}
public function isValid(): bool {}
}
/** Represents a watcher registered in a poll context */
final class PollWatcher {
private function __construct() {}
public function getHandle(): PollHandle {}
public function getWatchedEvents(): int {}
public function getTriggeredEvents(): int {}
public function getData() {}
public function hasTriggered(int $events): bool {}
public function isActive(): bool {}
public function modify(int $events, $data = null): void {}
public function modifyEvents(int $events): void {}
public function modifyData($data): void {}
public function remove(): void {}
}
/** Main poll context managing multiple watchers */
final class PollContext {
public function __construct(PollBackend $backend = PollBackend::Auto) {}
public function add(PollHandle $handle, int $events, $data = null): PollWatcher {}
public function wait(int $timeout = -1, int $maxEvents = -1): array {}
public function getBackend(): PollBackend {}
}
/** Exception thrown by poll operations */
class PollException extends Exception {}
?>Event Constants
Event constants can be combined with a bitwise OR.
Input Events
POLL_EVENT_READ – monitor readability (descriptor has data to read)
POLL_EVENT_WRITE – monitor writability (descriptor ready for writing)
POLL_EVENT_RDHUP – monitor remote half‑close (Linux epoll, must be requested)
POLL_EVENT_ONESHOT – one‑shot mode, automatically removes the watcher after a trigger
POLL_EVENT_ET – edge‑triggered mode (epoll/kqueue only)
Output Events
POLL_EVENT_READ – descriptor ready for reading
POLL_EVENT_WRITE – descriptor ready for writing
POLL_EVENT_ERROR – error condition (automatically monitored)
POLL_EVENT_HUP – hang‑up (peer closed connection, automatically monitored)
POLL_EVENT_RDHUP – peer closed write half (Linux epoll only)
READ, WRITE and RDHUP appear in both input and output; ERROR and HUP are output‑only.
Usage Examples
TCP Server Example
<?php
$poll = new PollContext();
$server = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr);
if (!$server) { die("Failed to create server: $errstr
"); }
stream_set_blocking($server, false);
$serverHandle = new StreamPollHandle($server);
$poll->add($serverHandle, POLL_EVENT_READ, ['type' => 'server']);
$clients = [];
echo "Server listening on port 8080
";
while (true) {
$events = $poll->wait(1000);
foreach ($events as $watcher) {
$data = $watcher->getData();
if ($data['type'] === 'server' && $watcher->hasTriggered(POLL_EVENT_READ)) {
$handle = $watcher->getHandle();
$server = $handle->getStream();
$client = stream_socket_accept($server, 0);
if ($client) {
stream_set_blocking($client, false);
$clientHandle = new StreamPollHandle($client);
$clientWatcher = $poll->add($clientHandle, POLL_EVENT_READ, ['type' => 'client']);
$clients[] = $clientWatcher;
echo "New client connected
";
}
} elseif ($data['type'] === 'client') {
$handle = $watcher->getHandle();
$stream = $handle->getStream();
if ($watcher->hasTriggered(POLL_EVENT_READ)) {
$buffer = fread($stream, 8192);
if ($buffer === false || $buffer === '') {
echo "Client disconnected
";
$watcher->remove();
fclose($stream);
} else {
echo "Received: $buffer";
fwrite($stream, "Echo: $buffer");
}
}
if ($watcher->hasTriggered(POLL_EVENT_HUP | POLL_EVENT_ERROR)) {
echo "Client connection error or hangup
";
$watcher->remove();
fclose($stream);
}
}
}
}
?>TCP Client Example
<?php
function sendAndReceive($poll, $watcher, $client, $message, $timeout = 5000) {
fwrite($client, $message);
echo "Sent: $message";
$events = $poll->wait($timeout);
if (empty($events)) { echo "Timeout waiting for response
"; return false; }
foreach ($events as $watcher) {
if ($watcher->hasTriggered(POLL_EVENT_READ)) {
$data = fread($client, 8192);
if ($data === false || $data === '') { echo "Server closed connection
"; return false; }
echo "Received: $data";
return true;
}
if ($watcher->hasTriggered(POLL_EVENT_ERROR | POLL_EVENT_HUP)) { echo "Connection error
"; return false; }
}
return true;
}
$poll = new PollContext();
$client = stream_socket_client('tcp://127.0.0.1:8080', $errno, $errstr, 30);
if (!$client) { die("Failed to connect: $errstr
"); }
stream_set_blocking($client, false);
$handle = new StreamPollHandle($client);
$watcher = $poll->add($handle, POLL_EVENT_READ);
$messages = ["Hello, Server!
", "How are you?
", "Goodbye!
"];
foreach ($messages as $message) {
if (!sendAndReceive($poll, $watcher, $client, $message)) { break; }
usleep(100000);
}
fclose($client);
echo "Client finished
";
?>Callback‑Based Event Loop
<?php
/** Generic event‑driven wrapper that dispatches to callbacks */
class EventLoop {
private PollContext $poll;
public function __construct() { $this->poll = new PollContext(); }
/** Add a stream with a callback */
public function addStream($stream, int $events, callable $callback): PollWatcher {
$handle = new StreamPollHandle($stream);
return $this->poll->add($handle, $events, ['callback' => $callback]);
}
/** Run the loop */
public function run(): void {
while (true) {
$events = $this->poll->wait();
foreach ($events as $watcher) {
$data = $watcher->getData();
$callback = $data['callback'];
$handle = $watcher->getHandle();
$stream = $handle->getStream();
$callback($watcher, $stream);
}
}
}
public function getContext(): PollContext { return $this->poll; }
}
$loop = new EventLoop();
$server = stream_socket_server('tcp://127.0.0.1:9090');
stream_set_blocking($server, false);
$loop->addStream($server, POLL_EVENT_READ, function($watcher, $stream) use ($loop) {
$client = stream_socket_accept($stream, 0);
if ($client) {
stream_set_blocking($client, false);
echo "Client connected
";
$loop->addStream($client, POLL_EVENT_READ, function($watcher, $stream) {
$data = fread($stream, 8192);
if ($data === false || $data === '') {
echo "Client disconnected
";
$watcher->remove();
fclose($stream);
return;
}
echo "Received: $data";
fwrite($stream, "Echo: $data");
});
}
});
echo "Callback‑based server listening on port 9090
";
$loop->run();
?>Modifying a Watcher
<?php
$poll = new PollContext();
$stream = fopen('php://temp', 'r+');
$handle = new StreamPollHandle($stream);
// Initially watch for read events
$watcher = $poll->add($handle, POLL_EVENT_READ, 'some data');
// Change to watch for write events only
$watcher->modifyEvents(POLL_EVENT_WRITE);
// Change both events and associated data
$watcher->modify(POLL_EVENT_READ | POLL_EVENT_WRITE, 'updated data');
// Update only the data
$watcher->modifyData('new data');
// Remove when finished
$watcher->remove();
?>Edge‑Triggered Mode
<?php
$poll = new PollContext();
$stream = stream_socket_client('tcp://example.com:80');
stream_set_blocking($stream, false);
$handle = new StreamPollHandle($stream);
// Use edge‑triggered mode (requires epoll or kqueue)
$watcher = $poll->add($handle, POLL_EVENT_READ | POLL_EVENT_ET);
while (true) {
$events = $poll->wait();
foreach ($events as $watcher) {
if ($watcher->hasTriggered(POLL_EVENT_READ)) {
while (($data = fread($stream, 8192)) !== false && $data !== '') {
// Process data until EAGAIN
process_data($data);
}
}
}
}
?>Explicit Backend Selection
<?php
try {
$poll = new PollContext(PollBackend::Poll);
echo "Using poll backend
";
} catch (PollException $e) {
echo "Poll backend not available: " . $e->getMessage() . "
";
exit(1);
}
// Show which backend is actually active
echo "Active backend: " . $poll->getBackend()->value . "
";
?>Internal API
Beyond the user‑space API, the RFC introduces an internal poll API for core and extensions, exposed via php_poll.h. The key functions are:
php_poll_create() – create a poll context with a chosen backend
php_poll_add() – add a file descriptor to be monitored
php_poll_modify() – change the events a descriptor is watched for
php_poll_remove() – stop monitoring a descriptor
php_poll_wait() – wait for events with an optional timeout
Extensions can implement php_poll_handle_ops to provide custom handle types, granting low‑level access to file descriptors and validity checks.
Backward‑Incompatible Changes
None. The RFC only adds new functionality without altering existing APIs.
Future Outlook
SocketPollHandle – native support for socket resources
CurlPollHandle – integration with libcurl for asynchronous HTTP
TimerHandle – timer‑based events for scheduling and timeouts
SignalHandle – signal notifications (SIGUSR1, SIGTERM, etc.)
ZTS signal‑handling improvements for multithreaded environments
FPM event‑loop optimisation – migrate PHP‑FPM to the internal poll API for better performance
Performance Considerations
The new poll API offers significant performance advantages over stream_select():
Scalability – epoll/kqueue efficiently handle thousands of descriptors.
O(1) complexity – modern backends operate in constant time regardless of descriptor count.
Fewer system calls – persistent registrations avoid the per‑call overhead of select().
Edge‑triggered mode – reduces calls further by notifying only on state changes.
Mechanisms such as epoll and kqueue are designed for high‑performance applications and can scale to tens of thousands of concurrent connections, whereas select() becomes impractical at that scale.
Reference: https://wiki.php.net/rfc/poll_api
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
