Fiber Multiplexing
OxPHP uses PHP Fibers to handle multiple HTTP requests concurrently on a single worker thread. When a request calls oxphp_sleep() or oxphp_async_await() (when the async pool is enabled), it suspends and the worker thread immediately picks up the next request. One worker can manage hundreds of in-flight requests without additional threads. A few contexts are exceptions; they are listed under Suspension points.
How it works
The scheduler runs many requests on one thread, giving each its own fiber and switching between them whenever a fiber suspends.
- A request arrives and the scheduler assigns it to a fiber: a lightweight execution context with its own stack and PHP state.
- The fiber runs the
oxphp_worker()handler. If the handler completes without suspending, the response is sent and the fiber is recycled with zero overhead compared to a single-request worker. - If the handler calls a suspending function (
oxphp_sleep(),oxphp_usleep(),oxphp_async_await()), the fiber yields back to the scheduler. - The scheduler picks up new incoming requests (creating new fibers) and resumes suspended fibers whose wait conditions are met (timer expired, async result ready).
- Each fiber's PHP state (
$_GET,$_POST,$_COOKIE,$_SERVER,$_FILES,$_REQUEST, response headers, output buffers, VM stack) is saved on suspension and restored on resumption, so a resuming fiber finds its own state rather than whatever ran in the meantime.
graph LR W["Worker Thread"] W --> A0["Fiber A: handling /api/users"] A0 --> A1["oxphp_sleep(0.5)"] A1 --> A2["suspended"] A2 --> A3["resumed"] A3 --> A4["response"] W --> B0["Fiber B: handling /api/orders"] B0 --> B1["oxphp_async_await($p)"] B1 --> B2["suspended"] B2 --> B3["resumed"] B3 --> B4["response"] W --> C0["Fiber C: handling /health"] C0 --> C1["response (no suspension, zero overhead)"]
Anything missing from that list is shared by the requests that overlap on a thread. The session is the one to know by name: $_SESSION, the session id and the open session behind them are one set per worker thread. A request the scheduler admits next to a suspended one that left a session open is handed that session rather than its own, and session_write_close() does not separate them (see Worker Mode → Session state). $_ENV goes further: it is worker state by design, so a write one request makes there is read by every later request on that worker, overlapping or not (see Superglobals).
Configuration
Fiber multiplexing activates automatically when worker mode is enabled. There are no additional environment variables.
| Variable | Default | Description |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Set to true and point ENTRY_FILE at a .php bootstrap to enable worker mode and fiber multiplexing |
PHP_WORKERS |
CPU / 2 (min 1) | Number of worker threads. Each thread runs its own independent scheduler with up to 256 concurrent fibers |
The maximum number of concurrent fibers per worker thread is 256. With 4 worker threads, OxPHP can handle up to 1,024 in-flight requests simultaneously.
Suspension points
These functions suspend the current fiber and let other requests run on the same thread:
| Function | What happens |
|---|---|
oxphp_sleep(float $seconds) |
Suspends the fiber for the given duration. Other fibers continue running, except in the contexts listed in the note below, where the call waits on the worker thread instead |
oxphp_usleep(int $microseconds) |
Same as oxphp_sleep() with microsecond granularity (minimum 1 ms) |
oxphp_async_await(int $promise_id) |
Suspends the fiber until the async task completes on the background thread pool. In the same contexts as oxphp_sleep(), it waits on the worker thread instead |
These functions do not suspend the fiber:
| Function | Behavior |
|---|---|
oxphp_stream_flush() |
Sends a chunk to the client immediately and returns. Use with oxphp_sleep() in SSE loops |
oxphp_finish_request() |
Sends the complete response and continues PHP execution. Does not yield |
PHP's built-in sleep() and usleep() block the entire worker thread by default. Either use oxphp_sleep() / oxphp_usleep(), or set RUNTIME_HOOKS=sleep to make the native builtins suspend the fiber as well. This is useful when third-party code calls sleep() directly. See Configuration.
The oxphp_* functions and the hooked builtins alike suspend only where a fiber switch is possible. Where it is not, they wait on the worker thread instead. The hooked builtins hand the call to the native sleep()/usleep(), so the wait, and what sleep() returns when a signal cuts it short, are the builtin's. That covers a declare(ticks) handler, pcntl's signal dispatch, a request's input build, a FILTER_CALLBACK from filter_input_array(), and the destructors run after a request comes apart; the list is open-ended and is given in full under Runtime hooks.
A fiber the application starts itself (with new Fiber, or through a scheduler it drives) is a separate case: OxPHP cannot resume a context it does not own, so a sleep inside one holds a worker thread. Size that share of the load as if the hook were off.
To confirm the server picked the setting up, read runtime_hooks from oxphp_server_info() or from the internal server's /config. An unrecognised environment variable is not an error anywhere.
PHP examples
Basic concurrent handling
Requests that don't suspend run at full speed with zero fiber overhead:
<?php
oxphp_worker(function () {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path === '/health') {
echo json_encode(['status' => 'ok']);
return; // No suspension — runs at full speed
}
if ($path === '/slow') {
oxphp_sleep(2.0); // Yields for 2 seconds — other requests run
echo "Done after 2s delay";
return;
}
echo "Hello";
});Non-blocking API calls
Combine oxphp_async() with oxphp_async_await() to make external API calls without blocking the worker:
<?php
oxphp_worker(function () {
// Dispatch two API calls to the async thread pool
$p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users'));
$p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders'));
// Await both — the fiber suspends, other requests run on this thread
$users = oxphp_async_await($p1);
$orders = oxphp_async_await($p2);
header('Content-Type: application/json');
echo json_encode(['users' => json_decode($users), 'orders' => json_decode($orders)]);
});SSE with cooperative sleep
Interleave Server-Sent Events with other requests using oxphp_stream_flush() and oxphp_sleep():
<?php
oxphp_worker(function () {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
for ($i = 0; $i < 30; $i++) {
echo "data: " . json_encode(['count' => $i, 'time' => time()]) . "\n\n";
oxphp_stream_flush(); // Send chunk now (does not suspend)
oxphp_sleep(1.0); // Yield for 1 second (other requests run)
}
});Blocking I/O
Fiber multiplexing is cooperative, not preemptive. A fiber that calls a blocking function freezes the entire worker thread: no other fibers on that thread can make progress.
Functions that block the worker
file_get_contents(),fopen(),fread()curl_exec(),curl_multi_exec()- PDO queries,
mysqli_query() - PHP's
sleep(),usleep()(useoxphp_sleep()instead, or enableRUNTIME_HOOKS=sleep, within the limits under Suspension points) - DNS resolution (
gethostbyname()) - Any synchronous network or disk I/O
Socket reads and stream_select() are the exception once RUNTIME_HOOKS=streams is enabled: both suspend the fiber instead of the thread, which covers clients that block on a PHP stream read (mysqlnd, phpredis, the HTTP stream wrappers) as well as loops waiting on several sockets at once. Writes, socket_select(), ext/curl, connecting and DNS resolution still block under streams alone. The net category also suspends connecting (persistent connects excepted), TLS handshakes and reads, reads on unix://, udp:// and udg:// sockets, socket pairs and the streams exported from sockets that are not TCP, and writes that wait for room, while socket_select(), ext/curl and DNS resolution still block. Whether the categories were installed at all is reported as runtime_hooks by oxphp_server_info() and by /config. See Configuration.
How to avoid blocking
Wrap blocking operations in oxphp_async() to run them on the async thread pool:
<?php
// WRONG — blocks the entire worker thread
$html = file_get_contents('https://example.com');
// CORRECT — runs on async pool, fiber yields
$promise = oxphp_async(fn() => file_get_contents('https://example.com'));
$html = oxphp_async_await($promise);For database queries:
<?php
$db = new PDO('mysql:host=db;dbname=app', 'root', 'secret');
// WRONG — blocks the worker
$users = $db->query('SELECT * FROM users WHERE active = 1')->fetchAll();
// CORRECT — query runs on async thread, fiber yields
$promise = oxphp_async(function () {
$db = new PDO('mysql:host=db;dbname=app', 'root', 'secret');
return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll();
});
$users = oxphp_async_await($promise);Database connections cannot be passed to oxphp_async() because objects are not serializable across threads. Create the connection inside the async closure, or use the direct query in the fiber if the query is fast enough that blocking is acceptable.
oxphp_async() requires ASYNC_WORKERS > 0. When the async pool is disabled (the default), calling oxphp_async() throws OxPHP\Async\AsyncException.
How fibers are recycled
Fiber C stacks are allocated once and reused across requests. When a fiber finishes handling a request, it is not destroyed; instead it suspends back to the scheduler and is added to a free list. The next request reuses the existing C stack, which avoids expensive memory allocation.
PHP VM stacks (used for function call frames) are kept the same way: each fiber's is allocated when the fiber first starts and reused by every request it serves. A request whose call chain outgrows it gets further pages, which are given back when those calls end.
Docker example
services:
app:
image: ghcr.io/oxphp/oxphp:0.13.0
ports:
- "80:80"
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=worker.php
- PHP_WORKERS=4
- ASYNC_WORKERS=8With this configuration, each of the 4 worker threads can handle up to 256 concurrent fibers, and blocking I/O is offloaded to 8 async worker threads.
Troubleshooting
Requests become slow when one request does heavy I/O
A fiber is calling a blocking function (database query, HTTP request, file read) without oxphp_async(). This blocks the entire worker thread.
Fix: Wrap blocking calls in oxphp_async():
<?php
$promise = oxphp_async(fn() => file_get_contents($url));
$result = oxphp_async_await($promise);"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."
The async pool is not configured. When ASYNC_WORKERS=0 (the default), all async functions throw OxPHP\Async\AsyncException.
Fix: Set ASYNC_WORKERS to a positive value:
ASYNC_WORKERS=8"Failed to dispatch async task" when using oxphp_async()
The async pool is running but at capacity.
Fix: Increase ASYNC_WORKERS or ASYNC_QUEUE_CAPACITY:
ASYNC_WORKERS=8
ASYNC_QUEUE_CAPACITY=512oxphp_sleep() not yielding to other requests
Fiber multiplexing only works in worker mode. In traditional mode, oxphp_sleep() falls back to a blocking usleep().
Fix: Enable worker mode by setting WORKER_MODE_ENABLED=true.
With worker mode already on, the call is somewhere a fiber switch is refused or OxPHP does not own the context, as listed under Suspension points. A fiber the application started itself (with new Fiber, or through a scheduler it drives inside the request) is the common way to land there, and no setting changes it.
High memory usage with many concurrent requests
Each fiber runs on a C stack of its own, sized by PHP's fiber.stack_size ini setting (2 MiB on 64-bit platforms when the setting is left unset), plus a PHP VM stack of its own. At the limit of 256 fibers per worker thread that comes to 512 MiB of C stack per thread, but as address space mapped from the operating system rather than memory in use: a page of a fiber's stack becomes resident only once code running on that fiber reaches it. A worker keeps every fiber it creates, stacks included, until it exits, and reuses them for later requests, so a fiber whose request once recursed deeply keeps those pages for as long as the worker lives.
Fix: If no request needs a deeper C stack than the size you pick, lower fiber.stack_size in php.ini to cap how much of its stack any one fiber can hold:
fiber.stack_size = 512KThat also shrinks the address space, to 128 MiB per worker thread at the fiber limit. Resident memory falls only where a request would have gone deeper than the new size, and that request now fails instead: the call that would go deeper throws Error: Maximum call stack size of … bytes … reached.
Limitations
- Fiber multiplexing works in worker mode only. It is not available in traditional mode
- Each worker has a hard limit of 256 fibers, which is not configurable at runtime
- Scheduling is cooperative only. There is no preemption, so CPU-bound code (tight loops, heavy computation) starves other fibers
- Blocking I/O blocks the thread. All blocking calls must be wrapped in
oxphp_async()for true concurrency - PHP's native
sleep()/usleep()are not fiber-aware. Useoxphp_sleep()/oxphp_usleep(), orRUNTIME_HOOKS=sleep; neither suspends where a fiber switch is refused or a userland scheduler owns the context (see Suspension points) oxphp_async_await_race()andoxphp_async_await_any()do not yield: they currently block even inside a fiber.oxphp_async_await_all()does suspend the fiber while waiting, so it is fiber-friendly. Forrace/any, use sequentialoxphp_async_await()calls if you need the thread to stay cooperative
Prompts for your agent
These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.
Make my request handlers safe for OxPHP's cooperative scheduler. Read the `features/fiber-multiplexing` page through the OxPHP documentation MCP server.
Go through this repository for calls that block a worker thread instead of suspending the fiber, and tell me which ones to move onto the async pool, which a runtime hook already covers, and which are short enough to leave alone.See also
- Worker Mode: persistent PHP workers and the
oxphp_worker()API - Async Promises: background thread pool for offloading blocking I/O
- SSE: real-time streaming combined with fiber-based cooperative sleep
- PHP Functions:
oxphp_sleep(),oxphp_usleep(), and other fiber-aware functions - Configuration Reference:
WORKER_MODE_ENABLED,ENTRY_FILE,PHP_WORKERS,ASYNC_WORKERS