From stream_set_blocking to Your Own Socket Loop
Before using ReactPHP or Swoole in production, it is worth looking at what actually happens underneath. Non-blocking I/O in PHP relies on the same operating system primitives as in any other language: sockets running in non-blocking mode, and stream_select signaling which sockets are ready. Understanding these fundamentals means every async framework becomes clear at a glance.
Table of Contents
- 1. What Non-Blocking I/O Actually Means
- 2. The Classic Blocking Model in PHP
- 3. Making Streams Non-Blocking with stream_set_blocking
- 4. stream_select: Monitoring Multiple Sockets at Once
- 5. Building a Minimal Non-Blocking TCP Server
- 6. What Happens Under the Hood: select, poll and epoll
- 7. Handling Partial Reads and Writes Correctly
- 8. Pitfalls: Busy Waiting and Forgotten Timeouts
- 9. Non-Blocking I/O Compared to Higher Abstractions
- 10. Summary
- 11. FAQ
1. What Non-Blocking I/O Actually Means
Non-blocking I/O describes a model where a read or write operation on a socket or stream returns immediately, regardless of whether data is actually available. Instead of the calling process waiting until data arrives, it gets an answer right away, either the available data or an indication that nothing is currently present. This single design decision is the foundation for nearly every framework offering concurrent processing in PHP, from ReactPHP to Swoole.
The opposite is blocking I/O, PHP's default behavior: a call like fread() on a socket with no available data halts the entire process until data arrives or a timeout occurs. For a single request that is unproblematic, but for a process meant to serve hundreds of connections concurrently, blocking I/O becomes the limiting factor, because every waiting connection stalls the entire process.
Non-blocking I/O alone does not solve the problem completely, it only provides the building block: sockets that never wait. Only in combination with a mechanism that monitors which of many sockets is currently ready does a working event loop model emerge. This article builds exactly that mechanism from scratch with plain PHP, without Swoole or ReactPHP, to fully grasp the principle.
2. The Classic Blocking Model in PHP
By default, every stream or socket opened in PHP is blocking. A call to fsockopen() followed by fread() halts the process until the remote server responds or the connection times out. For processing one sequential request after another, that is exactly the expected behavior, and in the vast majority of PHP applications it is entirely sufficient.
Blocking I/O only becomes problematic once multiple independent connections need to be served concurrently within the same process, for instance in a hand-rolled TCP server or a client that has to talk to ten different APIs at once. Without non-blocking I/O, you would either need to spawn a separate process or thread per connection, which costs resources, or replace the blocking calls with non-blocking alternatives.
3. Making Streams Non-Blocking with stream_set_blocking
The function stream_set_blocking($stream, false) is the central switch to put an existing PHP stream into non-blocking mode. After this call, fread() returns immediately even if no data is available, in that case as an empty string. This means: an empty return value from fread() under non-blocking I/O is not an error and not necessarily the end of the connection, but simply the statement "no data right now".
This ambiguity is a common beginner mistake: an empty string from fread() in non-blocking mode can mean either "no data currently" or "connection closed". The distinction is made via feof($stream), which explicitly checks whether the end of the stream has been reached. Anyone forgetting this check builds loops that misinterpret a closed connection as "still waiting for data" and keep running forever.
<?php
declare(strict_types=1);
$stream = stream_socket_client('tcp://api.internal:8080', $errno, $errstr, 5);
if ($stream === false) {
throw new \RuntimeException("Connection failed: {$errstr} ({$errno})");
}
// Switch the stream to non-blocking mode — fread() returns immediately.
stream_set_blocking($stream, false);
fwrite($stream, "GET /status HTTP/1.1\r\nHost: api.internal\r\n\r\n");
$buffer = '';
while (!feof($stream)) {
$chunk = fread($stream, 8192);
if ($chunk === '' || $chunk === false) {
// No data available right now — this is normal, not an error.
usleep(10000); // avoid a pure busy-loop while polling
continue;
}
$buffer .= $chunk;
}
echo "Received " . strlen($buffer) . " bytes" . PHP_EOL;
4. stream_select: Monitoring Multiple Sockets at Once
The example above works for a single stream but does not scale: a loop with usleep() for each of ten sockets would either waste CPU unnecessarily or introduce unnecessary latency. The solution is stream_select(), PHP's equivalent of the POSIX select() syscall. This function accepts arrays of streams to monitor for readability, writability or errors, and blocks efficiently until at least one of them is ready, or until a timeout is reached.
The decisive advantage over polling with usleep(): stream_select() delegates the waiting to the operating system, which is efficiently notified internally via interrupts instead of actively polling in a loop. For real non-blocking I/O with many concurrent connections, stream_select() is therefore the central building block for keeping CPU load low while still being able to react to arbitrarily many sockets.
<?php
declare(strict_types=1);
// Multiple non-blocking connections monitored in a single loop.
$sockets = [
'a' => stream_socket_client('tcp://service-a.internal:9001', $e1, $s1, 5),
'b' => stream_socket_client('tcp://service-b.internal:9002', $e2, $s2, 5),
'c' => stream_socket_client('tcp://service-c.internal:9003', $e3, $s3, 5),
];
foreach ($sockets as $socket) {
stream_set_blocking($socket, false);
}
$results = [];
while (count($results) < count($sockets)) {
$read = array_diff_key($sockets, $results);
$write = null;
$except = null;
// Blocks efficiently until at least one stream is readable, or 2s pass.
$ready = stream_select($read, $write, $except, 2);
if ($ready === false) {
throw new \RuntimeException('stream_select failed');
}
if ($ready === 0) {
continue; // timeout, loop again — no busy waiting happened
}
foreach ($read as $key => $socket) {
$chunk = fread($socket, 8192);
if ($chunk !== '' && $chunk !== false) {
$results[$key] = $chunk;
}
}
}
echo sprintf('Collected responses from %d services' . PHP_EOL, count($results));
5. Building a Minimal Non-Blocking TCP Server
The same building blocks that let a client talk to multiple servers in parallel can be mirrored for a server that serves multiple clients concurrently, without spawning a separate process per client. The server opens a listening socket with stream_socket_server(), puts it into non-blocking mode as well, and handles new connections and existing client sockets in the same stream_select() loop.
This pattern is conceptually exactly what ReactPHP and similar libraries do under the hood, just without the abstraction layers of promises and event emitters. Anyone who has built this minimal server once immediately understands why ReactPHP offers a Loop class, why streams communicate via event names like data and close, and why non-blocking I/O in PHP fundamentally remains single-threaded, even with hundreds of concurrent connections.
<?php
declare(strict_types=1);
$server = stream_socket_server('tcp://0.0.0.0:9000', $errno, $errstr);
if ($server === false) {
throw new \RuntimeException("Server bind failed: {$errstr}");
}
stream_set_blocking($server, false);
$clients = [];
while (true) {
$read = $clients;
$read[] = $server;
$write = null;
$except = null;
if (stream_select($read, $write, $except, 1) === false) {
break;
}
foreach ($read as $socket) {
if ($socket === $server) {
// New incoming connection — accept without blocking.
$client = stream_socket_accept($server, 0);
if ($client !== false) {
stream_set_blocking($client, false);
$clients[(int) $client] = $client;
}
continue;
}
$data = fread($socket, 4096);
if ($data === '' || $data === false) {
// Client closed the connection — clean up.
fclose($socket);
unset($clients[(int) $socket]);
continue;
}
fwrite($socket, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
}
}
6. What Happens Under the Hood: select, poll and epoll
stream_select() internally calls different operating system mechanisms depending on the platform to implement non-blocking I/O efficiently. On Linux, PHP historically uses the POSIX select() syscall, which passes a bitfield of all monitored file descriptors to the kernel. The problem: select() scales poorly, because the kernel has to scan the entire bitfield again on every call, a linear cost with the number of descriptors.
Modern event loop implementations, such as in Swoole or ReactPHP's ext-event extension, instead use epoll on Linux, which solves this scaling problem by having the kernel itself maintain a list of ready descriptors and return only those on every call, instead of checking all of them. For non-blocking I/O with a few dozen connections, the difference is barely noticeable, but with tens of thousands of concurrent connections, select() becomes a real bottleneck, which is why production async servers in PHP almost always use epoll-based implementations.
7. Handling Partial Reads and Writes Correctly
A central aspect of non-blocking I/O that most introductions omit: both fread() and fwrite() can process fewer bytes than requested, without that being an error. An fwrite($stream, $data) with a large payload might only actually write part of it into the socket buffer, while the rest is silently discarded if the return value is not checked. For reliable non-blocking I/O, every write must be repeated in a loop until all data has actually been written.
The same applies mirrored for fread() with larger messages: a 10-kilobyte response rarely arrives in a single call, but is spread across multiple reads that must be assembled in application code. Anyone ignoring this fragmentation and assuming a single fread() call always delivers the full message produces bugs that appear sporadically under load and with larger payloads, and are hard to reproduce.
<?php
declare(strict_types=1);
// Reliable non-blocking write: retries until the full payload is sent.
function writeAll($stream, string $data): void
{
$total = strlen($data);
$written = 0;
while ($written < $total) {
$bytes = fwrite($stream, substr($data, $written));
if ($bytes === false) {
throw new \RuntimeException('Write failed');
}
if ($bytes === 0) {
// Socket buffer full — wait briefly, then retry.
$write = [$stream];
$read = null;
$except = null;
stream_select($read, $write, $except, 0, 50000);
continue;
}
$written += $bytes;
}
}
8. Pitfalls: Busy Waiting and Forgotten Timeouts
The most common mistake when getting started with non-blocking I/O is a loop without any waiting mechanism, continuously calling fread() on empty sockets. This busy-waiting variant permanently loads a CPU core to one hundred percent, even when no data is flowing, a pattern that quickly triggers alerts and unnecessary costs in production environments. stream_select() with a sensible timeout is the correct alternative, since the process actually sleeps meanwhile instead of actively polling.
A second pitfall is ignoring dropped connections. A remote server that closes the connection without sending a signal PHP can clearly interpret leads to sockets that permanently remain in the stream_select() monitoring list even though no communication is possible anymore. Regular health checks and explicit timeouts per connection, not just for stream_select() itself, prevent dead connections from silently tying up resources.
9. Non-Blocking I/O Compared to Higher Abstractions
Raw non-blocking I/O with stream_select() is instructive, but rarely the right choice for production applications, because higher-level abstractions already implement the same foundation robustly. The following comparison shows when each level is appropriate.
| Level | Abstraction | When Appropriate |
|---|---|---|
| Pure stream_select() | None, manual management | Learning, very specific protocols |
| ReactPHP Event Loop | Promises, event emitters | Async clients, small servers |
| Swoole Coroutines | Automatic hooking, channels | High-load servers, many connections |
| Fibers (native) | Scheduler self-written | Custom libraries, frameworks |
The practical value of implementing raw non-blocking I/O yourself once is not in using it in production, but in immediately understanding every higher-level framework. Anyone who knows that ReactPHP's event loop ultimately just wraps stream_select() or epoll debugs async problems in ReactPHP applications considerably faster, because they know the underlying semantics instead of treating the abstraction as a black box.
Mironsoft
Network programming and performance depth in PHP
Connection issues in a custom-built PHP network application?
We analyze existing socket and streaming implementations, find busy-waiting patterns and missing partial-read handling, and advise on choosing between raw non-blocking I/O and higher-level frameworks.
Code Review
Analyzing existing socket loops for typical non-blocking mistakes
Performance Diagnostics
Identifying and fixing CPU load caused by busy waiting
Framework Advice
Finding the right abstraction level between stream_select, ReactPHP and Swoole
10. Summary
Non-blocking I/O in PHP relies on two simple building blocks: sockets that never wait thanks to stream_set_blocking(), and stream_select(), which efficiently signals which of multiple sockets is currently ready. These two functions are enough to build a minimal server or client that serves multiple connections concurrently, without spawning a separate process for each. Under the hood, modern implementations use more efficient mechanisms like epoll instead of the plain select() syscall, to scale linearly even with tens of thousands of connections.
For production use, higher-level abstractions like ReactPHP or Swoole are almost always the better choice, because they have already solved partial reads, error handling and timeout logic robustly. The value of implementing non-blocking I/O yourself with plain PHP once lies in understanding each of these frameworks from the ground up, instead of treating them as an opaque black box when a problem arises in production.
Non-Blocking I/O in PHP — The Essentials at a Glance
stream_set_blocking
Puts a stream into non-blocking mode, reads return immediately even without data.
stream_select
Efficiently monitors multiple sockets via the operating system, instead of actively polling with busy waiting.
Partial Reads/Writes
fread() and fwrite() can deliver fewer bytes than expected, loops with return value checks are mandatory.
select vs. epoll
Modern frameworks use epoll instead of select, to scale linearly with very many concurrent connections.