The PHP Concurrency Model
Understand blocking I/O and where async helps.
The PHP Concurrency Model is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
PHP Is Synchronous by Default
Classic PHP runs one request per process, top to bottom, with blocking I/O. A file_get_contents() to a slow API freezes the entire script until the bytes arrive. The shared-nothing model (a fresh process per request, state torn down at the end) is wonderfully simple — but it means a single worker can do exactly one thing at a time. This lesson maps where that hurts and where async helps.
Blocking I/O, Measured
Watch wall-clock time dominated by waiting, not computing. Three sequential 1-second requests take ~3 seconds even though the CPU is idle the whole time.
<?php
$start = microtime(true);
foreach (['a', 'b', 'c'] as $job) {
usleep(1_000_000); // simulate a 1s blocking network call
echo "done $job\n";
}
printf("elapsed: %.2fs\n", microtime(true) - $start); // ~3.00sCPU-bound vs I/O-bound
Concurrency only helps I/O-bound work — time spent waiting on the network, disk, or database. For CPU-bound work (hashing, image resizing, parsing), a single thread is already saturated; async just adds scheduling overhead. Diagnose first:
- Many slow HTTP calls / DB queries → async wins big.
- Tight numeric loops → use parallelism (processes/threads), not an event loop.
How PHP Has Scaled So Far
Historically PHP scaled with process-level concurrency: PHP-FPM keeps a pool of worker processes; each handles one request at a time, the OS multiplexes them. This is real concurrency across requests but offers nothing within a request — you still cannot fan out 50 API calls in parallel inside one handler with vanilla blocking code.
The Event Loop Idea
Async PHP introduces an event loop: a single thread that registers I/O operations as non-blocking, then sleeps in stream_select()/epoll until any of them is ready, dispatching callbacks as data arrives. One thread juggles thousands of sockets because it never sits idle waiting on a single one.
<?php
// Conceptual loop: poll many non-blocking streams at once
$streams = openManyNonBlockingSockets();
while ($streams) {
$read = $streams; $write = $except = [];
stream_select($read, $write, $except, null); // sleeps until ANY is ready
foreach ($read as $s) {
handleReadyStream($s); // runs only the sockets that have data now
}
}Cooperative, Not Preemptive
Async PHP is cooperative: tasks run until they voluntarily yield at an I/O point. There is no preemption, so a long blocking call or a tight CPU loop stalls the whole loop — every other task starves. Golden rule: never call blocking functions (sleep, blocking PDO, file_get_contents) inside an event-loop task. Use the loop's async equivalents.
Three Approaches in PHP
Modern PHP offers a layered toolkit:
- Fibers (PHP 8.1, core) — a low-level primitive to pause/resume a call stack. The building block, not a full framework.
- ReactPHP / Amp — userland event loops built on streams (and now Fibers) for non-blocking I/O without extensions.
- Swoole / OpenSwoole — a C extension providing coroutines, a high-performance server, and hooked blocking calls.
Detect what is available at runtime before choosing a strategy.
<?php
echo 'Fibers: ' . (class_exists('Fiber') ? 'yes' : 'no'), PHP_EOL;
echo 'Swoole ext: ' . (extension_loaded('swoole') ? 'yes' : 'no'), PHP_EOL;
echo 'parallel ext: ' . (extension_loaded('parallel') ? 'yes' : 'no'), PHP_EOL;
echo 'PHP ' . PHP_VERSION, PHP_EOL;Promises Model Future Values
Before Fibers, async PHP exposed pending results as promises: a placeholder for a value that resolves later. You attach callbacks via then(). It works but leads to nested chains; Fibers let libraries hide this behind synchronous-looking await.
<?php
use React\Promise\Promise;
$promise = new Promise(function ($resolve) {
// resolved later when I/O completes
$resolve(42);
});
$promise->then(function ($value) {
echo "got $value\n";
});Concurrency vs Parallelism
Be precise:
- Concurrency — many tasks in progress, interleaved on one thread (event loop). Great for I/O.
- Parallelism — many tasks executing simultaneously on multiple cores (processes, the
parallelextension, Swoole task workers). Needed for CPU work.
An event loop gives concurrency, not parallelism. The snippet below shows process-level parallelism with proc_open — three workers run on separate OS processes at once.
<?php
$procs = [];
foreach (range(1, 3) as $i) {
// Each worker is a separate OS process -> real parallelism
$procs[] = proc_open(
"php -r 'usleep(500000); echo \"worker $i done\\n\";'",
[1 => ['pipe', 'w']], $pipes
);
}
foreach ($procs as $p) { proc_close($p); }
echo "all workers launched in parallel\n";When NOT to Reach for Async
Async has costs: a long-lived worker means PHP no longer wipes state between requests, so memory leaks, static caches, and connection staleness become your problem. If a request handler makes one DB query and returns, PHP-FPM is simpler and just as fast. Reach for async when you have I/O fan-out, WebSockets/long polling, streaming, or a persistent server need.
True Parallelism: ext-parallel and Processes
For CPU-bound fan-out, PHP can run real OS-level parallelism. The parallel extension spins up worker threads with isolated memory; alternatively proc_open/pcntl_fork spawn processes. Unlike an event loop, these use multiple cores simultaneously — the right tool when the bottleneck is computation, not waiting.
<?php
// Requires ext-parallel — runs tasks on separate threads/cores
use parallel\Runtime;
$runtime = new Runtime();
$future = $runtime->run(function (): int {
$sum = 0;
for ($i = 0; $i < 1_000_000; $i++) $sum += $i; // CPU work on another core
return $sum;
});
echo $future->value(), PHP_EOL;Quick Check
What happens if you call blocking sleep(2) inside an event-loop task?
Recap
You now have a mental model of PHP concurrency:
- Default PHP is synchronous, blocking, shared-nothing; FPM scales across requests, not within one.
- Async helps I/O-bound work via a single-threaded event loop using non-blocking streams.
- The model is cooperative — never block the loop; long CPU work needs parallelism, not concurrency.
- The PHP toolkit: Fibers (primitive), ReactPHP/Amp (userland loops + promises), Swoole (coroutine extension).
Frequently asked questions
Is the “The PHP Concurrency Model” lesson free?
Yes — the full text of “The PHP Concurrency Model” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “The PHP Concurrency Model”?
Understand blocking I/O and where async helps. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “The PHP Concurrency Model” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The PHP Concurrency Model
- PHP 8.1 Fibers
- Event Loops with ReactPHP
- High-Performance Servers with Swoole