Swoole is a PHP extension that turns PHP into a long-running, asynchronous runtime, much like Node.js or Go. With PHP-FPM, every request starts from scratch, blocks on every database query or HTTP call, and throws everything away when it ends. A Swoole application starts once, keeps objects and connections in memory between requests, and runs thousands of concurrent tasks in a single process.
This repository teaches Swoole through small, runnable examples, one concept per script. Every example runs in the provided Docker containers, and nearly every one is covered by an automated test that runs in CI.
- Supported versions
- Quick start
- How to run the examples
- Key concepts in 60 seconds
- Start here: why Swoole?
- Coroutine basics
- Coordinating coroutines
- Runtime hooks
- Coroutine clients
- Servers
- Processes and shared memory
- Signals and the event loop
- Timers and cron jobs
- Putting it all together
- Pitfalls and advanced topics
- Testing Swoole code
- License
All the examples are written for PHP 8.4+ and Swoole 6.2+, and tested on PHP 8.4 with Swoole 6.2. The Docker
images, the Composer requirements, and the CI workflows all use these versions; the examples may not work on older
versions of PHP or Swoole.
You don't need PHP, Swoole, or Composer installed locally: the only requirement is Docker with Compose v2 (the
docker compose command).
git clone https://github.com/deminy/swoole-by-examples.git
cd swoole-by-examples
docker compose up -d # give it a few seconds for the servers to start
# Run a first example: 2,000 coroutines that each sleep for 1 second, all finishing in about 1 second.
docker compose exec -t client bash -c "time ./csp/coroutines/many-coroutines-in-a-loop.php"docker compose up -d starts three PHP containers, plus the redis, mysql, and postgresql services that the
database and Redis examples connect to. The PHP containers mount this repository at /var/www, with /var/www/examples
as the working directory (which is why run commands use paths like ./csp/channel.php):
| Container | What runs there | How to use it |
|---|---|---|
client |
Standalone scripts, and clients that talk to the servers | docker compose exec -t client bash -c "./csp/channel.php" |
server |
8 long-running example servers (HTTP, WebSocket, TCP, UDP, ...), started automatically by Supervisord and restarted whenever a PHP file under examples/ changes |
Nothing to start: connect to server:<port> from the client container |
zts |
The thread examples, which need a thread-safe (ZTS) build of PHP | docker compose exec -t zts php ./threads/map.php |
- Every example documents its exact run command in its docblock, along with any extra steps. The "Run from" column
in the tables below says where to run it:
client,server,zts, or "auto-started" for the servers that are already running. - Run examples from the
clientcontainer unless the docblock says otherwise. The two images differ: for example, onlyclienthas themysqliextension, and onlyserverhas APCu and System V message queues. - Server ports are not published to your host (except port 9801, for Rock Paper Scissors),
so send requests from the
clientcontainer, e.g.docker compose exec -t client curl -i http://server:9501. - To follow the servers' output, run
docker compose logs -f server. To open a shell, rundocker compose exec -ti client bash(orserver). To stop everything, rundocker compose down.
The images are built from the official Swoole image
(phpswoole/swoole:6.2-php8.4) with a few extra extensions and tools; see dockerfiles/. The repository
swoole/docker-swoole has more examples of using that image.
- Coroutine: a lightweight function that can pause and resume. When a coroutine waits on I/O (a query, an HTTP call, a sleep), Swoole runs another coroutine instead of blocking the whole process. Thousands of them can run in a single process.
- Event loop (scheduler): the part of Swoole that decides which coroutine runs next, and wakes coroutines up when their I/O is ready.
- CSP and channels: the concurrency style Swoole shares with Go (CSP stands for Communicating Sequential Processes). Coroutines pass data to each other through channels instead of sharing variables.
- Runtime hooks: the switch that makes ordinary blocking PHP code (
sleep(), PDO, mysqli, curl, phpredis, file functions, ...) non-blocking inside coroutines, without changing the code.Swoole\Coroutine\run()turns them on for a script; a server turns them on with itshook_flagssetting. - Servers: Swoole has built-in HTTP, WebSocket, TCP, UDP, and other servers, with no Nginx or PHP-FPM in front. A server runs several worker processes (managed by a manager process), and your code reacts to events such as "a request arrived".
- Processes and shared memory: for CPU-bound work or isolation, Swoole manages pools of worker processes, which
share data through shared-memory structures (
Swoole\Table,Swoole\Atomic) and locks.
The same work, done the PHP-FPM way and the Swoole way. Read these in order.
| Example | What it shows | Run from |
|---|---|---|
| Blocking I/O | A plain PHP script whose two simulated I/O calls run one after the other, taking about 3 seconds | client |
| Non-blocking I/O (debug version) | The same work in two coroutines that wait at the same time, taking about 2 seconds; the debug version prints the order in which they interleave | client |
| Blocking vs non-blocking | A function that starts a coroutine returns as soon as that coroutine pauses in a non-blocking way, while the coroutine keeps running in the background | client |
| 1,000,000 coroutines | How cheap coroutines are: start a million of them in one process, each sleeping for 5 seconds (needs about 8 GB of RAM) | client |
| Example | What it shows | Run from |
|---|---|---|
| Ways to create a coroutine | go(), Swoole\Coroutine::create(), Co::create(), and the other equivalent ways to start a coroutine |
client |
| Callback types | Any PHP callable (closure, function name, static or instance method) can be the body of a coroutine | client |
| Many coroutines in a loop | 2,000 one-second sleeps finish in about 1 second instead of about 2,000 | client |
| Nested coroutines (execution order) | Coroutines started inside other coroutines, and the order in which they run | client |
| Yield and resume | Pause a coroutine by hand, and resume it later from another coroutine | client |
| Cancel a coroutine | Swoole\Coroutine::cancel() interrupts a coroutine's sleep or channel wait from another coroutine, so it can stop early |
client |
| Block a coroutine | Waiting (e.g. sleep() or Channel::pop()) pauses only the current coroutine; the rest of the process keeps running |
client |
defer |
Register cleanup callbacks that run, in reverse order, when a coroutine finishes | client |
| Context | Per-coroutine storage that is cleaned up automatically when the coroutine ends; the coroutine-safe replacement for globals | client |
| Exit from a coroutine | exit() inside a coroutine throws Swoole\ExitException instead of ending the process; prefer throwing your own exception |
client |
| Example | What it shows | Run from |
|---|---|---|
| Channel | The main way coroutines pass data to each other; push() and pop() wait automatically when the channel is full or empty |
client |
WaitGroup |
Wait for a group of coroutines to finish, like Go's sync.WaitGroup |
client |
Barrier |
The same goal as WaitGroup with less code: the wait ends once every coroutine has released its reference to the barrier |
client |
| Lock across coroutines | Swoole\Coroutine\Lock: a mutex that suspends only the waiting coroutine, not the whole process |
client |
Runtime hooks make existing blocking PHP functions and extensions (curl, PDO, mysqli, Redis, sleep(), ...) run
concurrently inside coroutines, without changing their code. In the database and Redis examples below, five
3-second operations finish in about 3 seconds instead of 15.
| Example | What it shows | Run from |
|---|---|---|
| Enable and disable runtime hooks | Turn coroutine support on and off in a standalone script (Swoole\Coroutine\run() turns all hooks on by default) |
client |
| Hook flags | Choose which kinds of blocking functions get hooked: coroutines with the hooks enabled run concurrently, the others block | client |
Both examples send their requests to the auto-started HTTP/1 server in the server container.
| Example | What it shows | Run from |
|---|---|---|
SWOOLE_HOOK_NATIVE_CURL (recommended) |
Hooks the real curl extension, including curl_multi_*(): six 2-second requests finish in about 2 seconds |
client |
SWOOLE_HOOK_CURL |
The older approach, implemented in Swoole Library; doesn't support curl_multi_*() |
client |
| Example | What it shows | Run from |
|---|---|---|
| mysqli | Five concurrent MySQL queries using the mysqli extension | client |
| PDO_MYSQL | Five concurrent MySQL queries using PDO | client |
| PDO_PGSQL | Five concurrent PostgreSQL queries using PDO | client |
| PDO_SQLITE | Five concurrent SQLite writes using PDO (the delay comes from waiting on SQLite's own database lock) | client |
| Example | What it shows | Run from |
|---|---|---|
| phpredis | Five concurrent slow Redis operations using the phpredis extension | client |
| predis | The same using the pure-PHP predis library | client, after composer global require predis/predis=~3.0 |
Once database calls run concurrently, a pool shares a bounded set of connections among the coroutines instead of opening one connection per coroutine.
| Example | What it shows | Run from |
|---|---|---|
| MySQL connection pool | 1,024 one-second queries through 128 pooled connections finish in about 8 seconds | client |
| PostgreSQL connection pool | The same pattern with PDO_PGSQL | client |
| Redis connection pool | The same pattern with phpredis | client |
For a custom pool, see crowdstar/vertica-swoole-adapter, which implements a connection pool for HP Vertica databases through ODBC.
Swoole's own non-blocking clients, for use inside coroutines. Most of them talk to the servers running in the server
container; the low-level socket example creates its own server socket.
| Example | What it shows | Run from |
|---|---|---|
| HTTP/1 client | Two requests made concurrently; the faster one finishes first | client |
| HTTP/2 client | Multiplex concurrent requests as streams over a single TCP connection | client |
| WebSocket client | Connect, send a message, and read the reply | client |
| TCP client | Query two TCP servers concurrently | client |
| UDP client | Send a datagram and read the reply | client |
| Low-level socket | Swoole\Coroutine\Socket: a server socket and two client sockets exchanging length-prefixed messages |
client |
Some servers below are auto-started in the server container, and their docblocks show how to send requests to them.
The others ("client" in the Run from column) start their own server on a random port, send it requests, print what
happened, and shut down, so they run like any other script.
A server runs in one of two modes: in SWOOLE_BASE mode (the default), each worker process accepts and handles its own
connections; in SWOOLE_PROCESS mode, a master process owns all the connections and forwards their data to the worker
processes, so any worker can reach any connection (see
WebSocket broadcasting).
| Example | What it shows | Run from |
|---|---|---|
| HTTP/1 server | Custom status codes, static file serving, and gzip compression | auto-started, port 9501 |
| Server events | Which callbacks (onStart, onWorkerStart, onReceive, onTask, ...) fire, in which process, and in what order, including during a reload |
client |
| Coroutines in a server | The enable_coroutine, task_enable_coroutine, and hook_flags settings, and why a server enables no runtime hooks by default |
client |
| Coroutine-style HTTP server | Swoole\Coroutine\Http\Server: a server that runs inside a coroutine in the current process, with no worker processes |
client |
| Hot reload | Load updated code into a running server with $server->reload(), which restarts the worker processes gracefully |
client |
| Example | What it shows | Run from |
|---|---|---|
| HTTP/2 server | A minimal HTTP/2 server | auto-started, port 9503 |
| HTTPS server | Serve HTTP over SSL/TLS, and see a client reject an untrusted certificate and accept a trusted one | client |
| Server-Sent Events (SSE) | Stream a response chunk by chunk over HTTP/1.1, the technique many AI chat apps use to stream text | client |
| WebSocket server | A minimal WebSocket server that replies to messages | auto-started, port 9504 |
| WebSocket broadcasting | Send a message to every connected client, across worker processes, as in a chat room | client |
| TCP server, event-driven style | Register callbacks such as onReceive, and let the server call them |
auto-started, port 9505 |
| TCP server, coroutine style | Write the accept/read/write loop yourself inside coroutines, like a Go server | auto-started, port 9507 |
| UDP server | An echo server for UDP datagrams | auto-started, port 9506 |
| UDP multicast | A UDP server that joins an IP multicast group and receives datagrams sent to the group address | client |
| Redis server | A server speaking the Redis protocol (minimal GET/SET), usable from any Redis client |
client |
| MQTT broker | A minimal publish/subscribe broker built on the open_mqtt_protocol setting, tested with the Mosquitto command-line clients |
client |
| Reverse proxy | A TCP-level reverse proxy relaying each connection to an upstream HTTP server | client |
| Example | What it shows | Run from |
|---|---|---|
| Listening on multiple ports | One server listening on two ports, each with its own callbacks | client |
| Different protocols on different ports | One server speaking HTTP on one port and raw TCP on another | client |
| Several protocols on the same port | HTTP/1, HTTP/2, and WebSocket served on one port | client |
| Example | What it shows | Run from |
|---|---|---|
| Heartbeat | The server closes connections that have sent nothing for a given number of seconds | client |
| TCP keepalive | Let the operating system probe idle connections and drop dead ones | client |
| Delayed receive (DDoS protection) | Delay reading from a new connection until your code approves it (enable_delay_receive and Server::confirm()) |
client |
| Example | What it shows | Run from |
|---|---|---|
| APCu caching | APCu works in Swoole the same way as in any PHP CLI application; per-worker request counters show that all worker processes share the cache | server |
Swoole\Process\Pool keeps a fixed set of worker processes running, restarting any that exit. The examples differ in
how work reaches the workers through IPC (inter-process communication). In the message-queue, TCP and Unix-socket
examples, one worker process plays the client and sends work to the others.
| Example | What it shows | Run from |
|---|---|---|
| Standalone pool | Workers just run your code; nothing is sent to them | client |
| Pool with a message queue | Send work to the pool through a System V message queue | server |
| Pool with a TCP socket | Send work to the pool over TCP | client |
| Pool with a Unix socket | Send work to the pool over a Unix socket | client |
| Detach a worker | Let a worker escape the pool manager's control to finish a long task at its own pace | client |
| Example | What it shows | Run from |
|---|---|---|
Swoole\Table |
A fixed-schema, in-memory table shared by all processes, with atomic counter updates | client |
| Atomic counter, unsigned 32-bit | Swoole\Atomic: a shared-memory counter that is safe to update from many processes |
client |
| Atomic counter, signed 64-bit | Swoole\Atomic\Long: the same for signed 64-bit integers |
client |
| Example | What it shows | Run from |
|---|---|---|
| Lock across processes | Swoole\Lock: a shared-memory mutex that blocks the whole waiting process (never use it across coroutines; see Deadlocks) |
client |
| Block one process with a lock | Block a process on a Swoole\Lock with a timeout |
client |
| Block processes with a lock | Block several processes on a shared Swoole\Lock, then release them one by one from another process |
client |
| Block and wake up a process with an atomic | One process blocks on a shared Swoole\Atomic with wait() (with and without a timeout) until another process wakes it up with wakeup() |
client |
Swoole 6 can also run PHP code in multiple threads (Swoole\Thread). Threads don't share PHP variables; they share
data through thread-safe containers instead. These examples need a thread-safe (ZTS) build of PHP, so they run in the
zts container.
| Example | What it shows | Run from |
|---|---|---|
| Lock across threads | Swoole\Thread\Lock: a mutex shared by threads |
zts |
| Shared map | Swoole\Thread\Map: four threads update one map; an atomic incr() never loses an update, a read-then-write does |
zts |
| Work queue | Swoole\Thread\Queue: worker threads take jobs from a shared queue, the basic shape of a thread pool |
zts |
| Example | What it shows | Run from |
|---|---|---|
| Default exit condition | Signal listeners alone don't keep a process running: it exits before any signal arrives | client |
| Custom exit condition | The exit_condition option keeps the event loop alive while signal listeners are registered |
client |
| Wait for a signal in a coroutine | Swoole\Coroutine\System::waitSignal() suspends the calling coroutine until a signal arrives or a timeout expires, with no callbacks |
client |
| Example | What it shows | Run from |
|---|---|---|
Swoole\Timer |
Run code after a delay or at a fixed interval, like JavaScript's setTimeout() and setInterval() |
client |
| Timers with coroutines | The same, implemented with plain coroutines and sleeps | client |
The examples below run recurring jobs (cron jobs) in seven different ways, without the system cron.
The scheduler is a program of its own, deployed and supervised independently.
| Example | What it shows | Run from |
|---|---|---|
| Using timers | Fixed-rate scheduling with Swoole\Timer |
client |
| Using a coroutine loop | Overlap-free scheduling with a plain coroutine sleep | client |
| Using an interruptible sleep | A channel-based sleep that a shutdown can interrupt instantly | client |
| Using a process pool | The schedule sharded across the workers of a process pool | client |
The scheduler runs inside an application server, following the server's lifecycle and sharing its state.
| Example | What it shows | Run from |
|---|---|---|
| Timer plus task workers | A timer in one server worker hands scheduled work to task worker processes, so slow jobs don't stall requests | client |
| Dedicated user process | An isolated scheduler process attached to a server (Server::addProcess()), with graceful shutdown on SIGTERM |
client |
| Interruptible sleep in a server | Let a cron job inside a web server run one last time when the server shuts down, instead of being cut off mid-interval | client |
| Example | What it shows | Run from |
|---|---|---|
| Integrated HTTP/1 server | One HTTP server that also runs cron jobs, and hands slow work to task worker processes (task(), taskwait(), taskWaitMulti(), taskCo()) |
auto-started, port 9502 |
| Integrated WebSocket server | A WebSocket server with separate processes for a cron job and for consuming a task queue | client |
| Rock Paper Scissors | The server holds the first two players' HTTP requests open, and answers all three players at once when the last one arrives, which PHP-FPM can't do | auto-started, port 9801 (also open on your host: http://127.0.0.1:9801?name=A) |
A deadlock happens when every coroutine is waiting and nothing can wake them up. When coroutines wait on Swoole's own
primitives (such as channels), Swoole detects this and reports it. When they block on something Swoole can't see, such
as a file lock or a Swoole\Lock, the program just hangs.
| Example | What it shows | Run from |
|---|---|---|
| Pop from an empty channel | The only coroutine waits for data that never comes | client |
| Push to a full channel | The only coroutine waits for space that never frees up | client |
| File locking | Lock a file that is already locked and never released: the program hangs, with no deadlock report | client |
Swoole\Lock across coroutines |
Acquire a locked Swoole\Lock from another coroutine: the program just hangs |
client |
| Server shutdown | Shut down a server while a coroutine is still running | client |
Each of these examples creates a deadlock on purpose, by pausing the only coroutine in the program forever.
| Example | What it shows | Run from |
|---|---|---|
| Default behavior | Swoole reports the deadlock and the coroutines involved | client |
| Deadlock check disabled | Turn off the deadlock report with enable_deadlock_check |
client |
| Custom exit condition | Replace the deadlock check with your own rule for when the program may exit (this demo waits forever) | client |
Coroutines normally switch only when one of them waits on I/O, so a CPU-heavy loop can starve the others. Read these in order.
| Example | What it shows | Run from |
|---|---|---|
| Non-preemptive scheduling | The default: a busy coroutine never gives up the CPU, so the other coroutine never runs | client |
| Preemptive scheduling | With swoole.enable_preemptive_scheduler on, Swoole interrupts the busy coroutine every few milliseconds so others get a turn |
client |
| Toggle the preemptive scheduler | Turn preemption off around a critical section, and back on afterwards | client |
With plain PHPUnit, tests run one at a time and outside of any coroutine, so tests that wait on I/O add up, and test code can't call coroutine APIs directly. deminy/counit runs each test method in its own coroutine instead, so tests that wait on I/O run concurrently.
This repository's own test suite uses it to run a test for nearly every example:
- phpunit.xml.dist registers the
Deminy\Counit\CounitExtensionextension. - The base test class tests/Support/ExampleTestCase.php extends
Deminy\Counit\TestCase. - There is one test class per topic under tests/. Most tests run their example as a subprocess and check its output, with at most 8 examples running at once; the auto-started servers are tested by connecting to them, either directly with Swoole's coroutine clients or through the client examples.
To run the tests (composer.json is at the repository root, hence the -w /var/www):
docker compose exec -T -w /var/www client composer install -n -q --no-progress
docker compose exec -T -w /var/www client ./vendor/bin/counit --testsuite client
docker compose exec -T -w /var/www server ./vendor/bin/counit --testsuite server
docker compose exec -T -w /var/www zts ./vendor/bin/counit --testsuite ztsAlmost every test is in the client suite. The server suite holds the tests for the examples that need PHP
extensions installed only in the server container (APCu and "sysvmsg"), and the zts suite holds the tests for the
thread examples. The client suite takes about a minute.
Why some tests run in a separate process, and which example isn't tested
- Tests for examples that may hang forever by design (the deadlock demos), that block the whole process (the
io/block-*-using-swoole-lock.phpexamples), or that depend on the preemptive scheduler run one at a time in a separate, non-coroutine process, using PHPUnit's#[RunInSeparateProcess]attribute andExampleTestCase::runIsolated(). Running many such subprocesses concurrently proved unreliable, so these trade speed for reliability, and they account for most of the suite's running time. csp/coroutines/benchmark.phpis skipped (it creates 1,000,000 coroutines).
For examples of unit-testing coroutine code in the same process, see the counit documentation.
The examples are licensed under CC BY-NC-ND 4.0; see LICENSE.txt.