Deep reference for writing production Swoole PHP code. Covers the long-running process mental model (object lifetimes, what breaks vs php-fpm, per-request state via Coroutine::getContext), runtime hook flags (what's hookable year-to-year, what isn't), parent/child priority gotcha, Channel/WaitGroup/Barrier/defer patterns, server canonical skeleton (SWOOLE_BASE default since 5.0, dispatch modes, graceful reload rules), connection pooling (try/finally + defer, Channel-as-pool), the full pitfall...
Installs into .claude/skills of the current project.
Are you the author of Swoole Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/abnegate-swoole-expert)
---
name: swoole-expert
description: Deep reference for writing production Swoole PHP code. Covers the long-running process mental model (object lifetimes, what breaks vs php-fpm, per-request state via Coroutine::getContext), runtime hook flags (what's hookable year-to-year, what isn't), parent/child priority gotcha, Channel/WaitGroup/Barrier/defer patterns, server canonical skeleton (SWOOLE_BASE default since 5.0, dispatch modes, graceful reload rules), connection pooling (try/finally + defer, Channel-as-pool), the full pitfalls catalog (blocking in coroutines, shared connections, pcntl, Channel deadlocks, cooperative != concurrent-safe, exception isolation), production tuning tables, testing entry point with Co\run, Swoole 6.x version notes (removed coroutine clients, multi-threading, io_uring, Lock API changes), and build flags. Use when writing or reviewing Swoole code (swoole/swoole-src, NOT openswoole) on Swoole 5.x/6.x with PHP 8.2+.
---
# Swoole Expert
Reference for **swoole/swoole-src** (NOT openswoole). Target: **Swoole 5.x/6.x**, **PHP 8.2+**. Assumes baseline coroutine/async knowledge. This file covers only what's house-specific, load-bearing, or shifts year-to-year.
Non-negotiable defaults:
- Prefer idiomatic `Co\run` / `go()` / `Co::defer` patterns
- Always enable `SWOOLE_HOOK_ALL` so native PHP I/O becomes coroutine-aware
- Never share a connection across coroutines -- use a pool
- Workers are **resident** processes, not short-lived like php-fpm
---
## 1. The long-running process mental model
Swoole workers are **resident PHP processes**. Unlike php-fpm, memory is NOT torn down between requests. Everything allocated at class, file, or worker scope survives across every request.
### Four object lifetimes
| Lifetime | Where created | Destroyed by |
|---|---|---|
| **Program-global** | Before `$server->start()` | Full process shutdown (`reload` will NOT refresh) |
| **Process-global** | In `onWorkerStart` | `max_request` reached, worker crash, or reload |
| **Session** | `onConnect` / first `onReceive` | `onClose` |
| **Request** | Inside the request handler | End of request |
If you allocated it in a hot path, assume it's a leak unless you can point to where it's freed.
### What does NOT work (vs php-fpm)
- **Superglobals**: `$_GET`, `$_POST`, `$_COOKIE`, `$_FILES`, `$_SERVER`, `$_REQUEST`, `$_SESSION` are NOT populated. Use `$request->get`, `$request->post`, `$request->cookie`, `$request->files`, `$request->server`, `$request->header`, `$request->rawContent()`.
- **`session_start()`** -- implement sessions with Redis/DB keyed by a cookie from `$request->cookie`.
- **`echo` / `print_r` / `var_dump`** -- go to stdout/log_file, NOT the client. Use `$response->write()` / `$response->end()`.
- **`exit()` / `die()`** -- kills the worker. Swoole 4.1+ converts to `Swoole\ExitException`. Never use for control flow.
- **`header()` / `setcookie()`** -- silent no-ops. Use `$response->header()`, `$response->cookie()`, `$response->status()`. Must be called BEFORE `$response->end()`.
- **Static properties / singletons / `global`** -- leak request state across coroutines. Use `Coroutine::getContext()`.
- **`pcntl_*`** -- forbidden in coroutines. Use `Swoole\Process` and `Process::signal()`.
- **Xdebug, phptrace, aop, molten, xhprof, phalcon** -- incompatible with coroutines. Disable them.
### Per-request state with `Coroutine::getContext()`
```php
use Swoole\Coroutine;
// Context is a Swoole\Coroutine\Context (extends ArrayObject),
// auto-destroyed when coroutine exits -- no cleanup needed.
$ctx = Coroutine::getContext();
$ctx['user_id'] = 42;
$ctx['requestId'] = bin2hex(random_bytes(8));
function innerFunction(): void
{
$ctx = Coroutine::getContext();
$userId = $ctx['user_id']; // works from anywhere in the same coroutine
}
```
Do NOT stash `$this` in the context -- it holds a strong reference and usually leaks the controller.
---
## 2. Coroutines
### Creating and running
```php
Swoole\Coroutine::create(callable $fn, mixed ...$args): int|false
go(callable $fn, mixed ...$args): int|false // short alias
Swoole\Coroutine\run(callable $fn): bool // top-level entry point
Co\run(callable $fn): bool // alias
```
All coroutine-creating APIs must run inside a **coroutine container** — either `Co\run()`, a server event callback, or a `Process`/`Process\Pool` worker with `enable_coroutine = true`. Nesting `run()` inside another `run()` is forbidden.
### Introspection
```php
Swoole\Coroutine::getCid(): int // -1 if outside coroutine
Swoole\Coroutine::getPcid(int $cid = 0): int|false
Swoole\Coroutine::exists(int $cid): bool
Swoole\Coroutine::list(): Swoole\Coroutine\Iterator
Swoole\Coroutine::stats(): array // coroutine_num, coroutine_peak_num, ...
Swoole\Coroutine::getBackTrace(int $cid = 0, int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array
Swoole\Coroutine::printBackTrace(int $cid = 0): void
Swoole\Coroutine::getElapsed(int $cid = 0): int // milliseconds alive
```
### Key non-obvious behaviors
### Parent/child priority gotcha
When you call `go()`, the **child starts immediately** and runs until its first yield. Only then does `go()` return to the parent:
```php
echo "a\n";
go(function () {
echo "b\n";
Co::sleep(0.1); // yields here
echo "d\n";
});
echo "c\n";
// Output: a, b, c, d
```
Coroutines have no parent/child lifecycle -- a parent exiting does not cancel or wait for children. Use `Barrier` or `WaitGroup` to wait.
### Scheduling yield points
A coroutine runs until: hooked I/O, `Co::sleep()`, `Channel::push()`/`pop()`, `Coroutine::yield()`/`suspend()`, `WaitGroup::wait()`/`Barrier::wait()`.
**A CPU-bound loop with no I/O monopolizes the worker.** Sprinkle `Coroutine::sleep(0)` to voluntarily yield, or dispatch CPU work to a task worker.
### Coroutine config
```php
Swoole\Coroutine::set(array $options): void // call BEFORE run() / Server->start()
```
| Option | Default | Note |
|---|---|---|
| `max_coroutine` | 100000 | Global limit |
| `stack_size` / `c_stack_size` | 2 MB | C stack per coroutine |
| `hook_flags` | 0 | One-click hooks, e.g. `SWOOLE_HOOK_ALL` |
| `enable_preemptive_scheduler` | false | Force preemption at 10 ms |
| `socket_connect_timeout` | | Default connect timeout |
| `socket_timeout` | | Read/write timeout |
| `dns_cache_expire` / `dns_server` | | DNS defaults |
| `enable_deadlock_check` | true | |
### Cancellation
```php
Swoole\Coroutine::cancel(int $cid, bool $throwException = false): bool // $throwException added 6.1
Swoole\Coroutine::isCanceled(): bool
Swoole\Coroutine::setTimeLimit(float $seconds): void // 6.2+
```
**Caveat**: `cancel()` cannot cancel file I/O coroutines; may segfault. Since 6.2 it can cancel in-flight io_uring ops.
---
## 3. Runtime hooks and flag reference
Hooks patch PHP's blocking stdlib to yield on I/O. Set **once** at bootstrap:
```php
Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);
// Or: $server->set(['hook_flags' => SWOOLE_HOOK_ALL]);
```
### Flag reference (ext-swoole 6.x)
| Flag | Covers |
|---|---|
| `SWOOLE_HOOK_TCP` | TCP streams, fsockopen, mysqlnd-based PDO_MYSQL/mysqli, predis, php-amqplib |
| `SWOOLE_HOOK_UNIX` | Unix-domain stream sockets |
| `SWOOLE_HOOK_UDP` / `SWOOLE_HOOK_UDG` | UDP / Unix datagram |
| `SWOOLE_HOOK_SSL` / `SWOOLE_HOOK_TLS` | TLS streams |
| `SWOOLE_HOOK_SLEEP` | `sleep`, `usleep` (>= 1ms), `time_nanosleep`, `time_sleep_until` |
| `SWOOLE_HOOK_FILE` | `fopen`, `fread`, `fwrite`, `file_get_contents`, `file_put_contents`, `unlink`, `mkdir`, `rmdir`. Uses AIO or **io_uring** in 6.0+ |
| `SWOOLE_HOOK_STREAM_FUNCTION` | `stream_select()` |
| `SWOOLE_HOOK_BLOCKING_FUNCTION` | `gethostbyname`, `shell_*` |
| `SWOOLE_HOOK_PROC` | `proc_open`, `proc_close`, `proc_get_status`, `proc_terminate` |
| `SWOOLE_HOOK_NATIVE_CURL` | Real libcurl coroutinized -- **use this**, not legacy `SWOOLE_HOOK_CURL`. Requires `--enable-swoole-curl`. Guzzle/Symfony HttpClient work transparently |
| `SWOOLE_HOOK_CURL` | **Legacy** partial reimplementation. Avoid |
| `SWOOLE_HOOK_SOCKETS` | ext-sockets. Auto-dropped in 6.1.6+ if ext-sockets not loaded |
| `SWOOLE_HOOK_STDIO` | STDIN/STDOUT/STDERR |
| `SWOOLE_HOOK_PDO_PGSQL` | `pdo_pgsql` (5.1+; 6.1.7 added timeout) |
| `SWOOLE_HOOK_PDO_ODBC` | `pdo_odbc` (5.1+) |
| `SWOOLE_HOOK_PDO_ORACLE` | `pdo_oci` (5.1+). **Constant is `_ORACLE`, not `_OCI`** |
| `SWOOLE_HOOK_PDO_SQLITE` | `pdo_sqlite` (5.1+) -- requires sqlite serialized/multi-thread |
| `SWOOLE_HOOK_PDO_FIREBIRD` | `pdo_firebird` (**new 6.2**) |
| `SWOOLE_HOOK_MONGODB` | MongoDB (**new 6.2** via `Swoole\RemoteObject\Server`) |
| `SWOOLE_HOOK_NET_FUNCTION` | coroutine `gethostbyname` (6.2+) |
| `SWOOLE_HOOK_ALL` | All of the above |
### Not hookable (block -- don't use in a coroutine)
- `mysql` extension (libmysqlclient)
- `mongo` / `mongodb` (mongo-c-client) -- use the 6.2+ MongoDB hook instead
- `php-amqp` (C AMQP ext -- but `php-amqplib` over streams works)
- Any extension bypassing PHP's streams layer
`pdo_mysql` and `mysqli` hookable **only in mysqlnd mode**. Check `php -m | grep mysqlnd`.
---
## 4. Concurrency primitives
### `Swoole\Coroutine\Channel`
```php
final class Swoole\Coroutine\Channel {
public int $capacity;
public int $errCode; // SWOOLE_CHANNEL_OK | _TIMEOUT | _CLOSED | _CANCELED
public function __construct(int $capacity = 1);
public function push(mixed $data, float $timeout = -1): bool;
public function pop(float $timeout = -1): mixed;
public function close(): bool;
public function length(): int;
public function isEmpty(): bool;
public function isFull(): bool;
public function stats(): array;
}
```
**Gotchas**:
- Pushing `false`/`null`/`0` is ambiguous -- `pop()` also returns `false` on close/timeout. Always check `$chan->errCode`.
- `close()` wakes ALL waiting producers/consumers; they return `false`.
- Create channels in `onWorkerStart`, not before `start()`.
**Channel as semaphore** (capacity N, pre-filled):
```php
$sem = new Channel(5);
for ($i = 0; $i < 5; $i++) $sem->push(true);
go(function () use ($sem) {
$sem->pop();
try { doLimitedWork(); } finally { $sem->push(true); }
});
```
### `Swoole\Coroutine\WaitGroup`
```php
final class Swoole\Coroutine\WaitGroup {
public function __construct(int $delta = 0);
public function add(int $delta = 1): void;
public function done(): void;
public function wait(float $timeout = -1): bool; // false on timeout
public function count(): int;
}
```
Always call `done()` in `finally` -- if a worker throws and skips `done()`, `wait()` hangs.
### `Swoole\Coroutine\Barrier` (preferred over WaitGroup)
```php
use Swoole\Coroutine\Barrier;
$barrier = Barrier::make();
for ($i = 0; $i < 4; $i++) {
go(function () use ($barrier) { // capturing $barrier bumps refcount
Co::sleep(0.5);
});
}
Barrier::wait($barrier); // by reference; nulled after wait
```
`wait()` takes the barrier **by reference**. If you forget `use ($barrier)` the child doesn't hold a ref and `wait()` returns immediately.
### `defer` -- Go-style cleanup
```php
go(function () {
$db = new PDO(/* ... */);
defer(fn() => $db = null); // runs on coroutine exit, LIFO order, even on exception
});
```
### Timers
```php
Swoole\Timer::tick(int $msec, callable $cb, mixed ...$params): int
Swoole\Timer::after(int $msec, callable $cb, mixed ...$params): int
Swoole\Timer::clear(int $timer_id): bool
Swoole\Timer::clearAll(): bool
Swoole\Timer::info(int $timer_id): ?array
Swoole\Timer::list(): Swoole\Timer\Iterator
Swoole\Timer::stats(): array
```
Callback: `function(int $timerId, mixed ...$params): void`. In a coroutine container, timer callbacks run inside a new coroutine automatically.
### Batch primitives
```php
Swoole\Coroutine\batch(array $tasks, float $timeout = -1): array // concurrent callables
Swoole\Coroutine\parallel(int $n, callable $fn): void // N copies of $fn
Swoole\Coroutine\map(array $list, callable $fn, float $timeout = -1): array
Swoole\Coroutine::join(array $cids, float $timeout = -1): bool // wait by cid
```
---
## 5. HTTP / WebSocket / TCP servers
### Canonical HTTP server skeleton
```php
<?php
declare(strict_types=1);
use Swoole\Http\{Server, Request, Response};
$server = new Server('0.0.0.0', 9501, SWOOLE_BASE);
$server->set([
'worker_num' => swoole_cpu_num() * 2,
'task_worker_num' => 4,
'task_enable_coroutine' => true,
'max_request' => 10_000,
'max_wait_time' => 60,
'reload_async' => true,
'hook_flags' => SWOOLE_HOOK_ALL,
'http_compression' => true,
'log_file' => '/var/log/swoole.log',
'log_level' => SWOOLE_LOG_INFO,
'package_max_length' => 8 * 1024 * 1024,
]);
$server->on('request', function (Request $request, Response $response): void {
$method = $request->server['request_method'] ?? 'GET';
$uri = $request->server['request_uri'] ?? '/';
$body = $request->rawContent();
$response->status(200);
$response->header('Content-Type', 'application/json');
$response->end(json_encode(['method' => $method, 'uri' => $uri]));
});
$server->on('workerStart', function (Server $server, int $workerId): void {
// Reset per-worker state, open DB/Redis pools here
});
$server->start();
```
### Server constructor
```php
new Swoole\Server(
string $host = '0.0.0.0',
int $port = 0,
int $mode = SWOOLE_BASE, // SWOOLE_BASE (default), SWOOLE_PROCESS, SWOOLE_THREAD (6.0+ ZTS)
int $sock_type = SWOOLE_SOCK_TCP
)
```
`Http\Server` and `WebSocket\Server` extend `Swoole\Server` with the same constructor. SSL: OR sock type with `SWOOLE_SSL`.
### All server events
```
onStart(Server $server) // master start; NOT in SWOOLE_BASE mode
onShutdown(Server $server)
onManagerStart(Server $server) // NOT in SWOOLE_BASE mode
onManagerStop(Server $server)
onBeforeReload(Server $server) // 4.5+
onAfterReload(Server $server) // 4.5+
onWorkerStart(Server $server, int $workerId)
onWorkerStop(Server $server, int $workerId)
onWorkerExit(Server $server, int $workerId) // fires when reload_async and worker is draining
onWorkerError(Server $server, int $workerId, int $workerPid, int $exitCode, int $signal)
onBeforeShutdown(Server $server) // 4.8+
onPipeMessage(Server $server, int $srcWorkerId, mixed $message)
// TCP
onConnect(Server $server, int $fd, int $reactorId)
onReceive(Server $server, int $fd, int $reactorId, string $data)
onPacket(Server $server, string $data, array $clientInfo) // UDP
onClose(Server $server, int $fd, int $reactorId)
// Task workers
onTask(Server $server, Swoole\Server\Task $task)
onFinish(Server $server, int $taskId, mixed $data)
// HTTP
onRequest(Swoole\Http\Request $request, Swoole\Http\Response $response)
// WebSocket
onHandShake(Http\Request $request, Http\Response $response): bool
onOpen(WebSocket\Server $server, Http\Request $request)
onMessage(WebSocket\Server $server, WebSocket\Frame $frame)
```
### Core server methods
```php
public function set(array $settings): bool;
public function on(string $event, callable $callback): bool;
public function start(): bool;
public function stop(int $workerId = -1): bool;
public function shutdown(): bool;
public function reload(bool $onlyReloadTaskworker = false): bool;
public function send(int|string $fd, string $data, int $serverSocket = -1): bool;
public function close(int $fd, bool $reset = false): bool;
public function exists(int $fd): bool;
public function pause(int $fd): bool;
public function resume(int $fd): bool;
public function task(mixed $data, int $workerIdx = -1, ?callable $finishCb = null): int|false;
public function taskwait(mixed $data, float $timeout = 0.5, int $workerIdx = -1): mixed;
public function taskCo(array $tasks, float $timeout = 0.5): array|false;
public function finish(mixed $data): bool;
public function sendMessage(mixed $message, int $dstWorkerId): bool;
public function addProcess(Swoole\Process $process): int|false;
public function getClientInfo(int $fd, int $reactorId = -1): false|array;
public function getWorkerId(): int|false;
public function stats(): array;
```
### `Swoole\Http\Request` properties
```php
public int $fd;
public int $streamId;
public array $header; // lowercase keys
public array $server; // request_method, request_uri, query_string, request_time, remote_addr, ...
public ?array $cookie;
public array $get;
public array $post;
public array $files;
public function rawContent(): string|false; // alias getContent()
public function getMethod(): string|false;
```
### `Swoole\Http\Response` methods
```php
public function status(int $httpCode, string $reason = ''): bool;
public function header(string $key, string|array $value, bool $format = true): bool;
public function cookie(string $name, string $value = '', int $expires = 0,
string $path = '/', string $domain = '', bool $secure = false, bool $httponly = false,
string $samesite = '', string $priority = ''): bool;
public function trailer(string $key, string $value): bool;
public function write(string $content): bool; // chunked; disables compression
public function end(?string $content = null): bool;
public function sendfile(string $filename, int $offset = 0, int $length = 0): bool;
public function redirect(string $location, int $httpCode = 302): bool;
public function detach(): bool;
// Upgrade to WebSocket from an HTTP server
public function upgrade(): bool;
public function push(Frame|string $data, int $opcode = WEBSOCKET_OPCODE_TEXT, int $flags = WEBSOCKET_FLAG_FIN): bool;
public function recv(float $timeout = 0): Frame|string|false;
public function close(): bool;
```
### Server modes
**Default mode changed from `SWOOLE_PROCESS` to `SWOOLE_BASE` in 5.0.** `SWOOLE_BASE` has no manager process -- workers accept directly. `SWOOLE_THREAD` (6.0+) requires ZTS PHP.
### Dispatch modes
| Mode | Constant | Use for |
|---|---|---|
| 1 | `SWOOLE_DISPATCH_ROUND` | Stateless, async only; `onConnect`/`onClose` suppressed |
| 2 | `SWOOLE_DISPATCH_FDMOD` | Stateful TCP (fd % worker_num) |
| 3 | `SWOOLE_DISPATCH_IDLE_WORKER` | Preemptive to idle worker -- recommended for HTTP |
| 4 | `SWOOLE_DISPATCH_IPMOD` | Sticky by client IP |
| 5 | `SWOOLE_DISPATCH_UIDMOD` | Sticky by `$server->bind($fd, $uid)` |
| 7 | `SWOOLE_DISPATCH_STREAM` | Workers `accept` themselves |
| 9 | `SWOOLE_DISPATCH_CO_REQ_LB` | Coroutine request LB (best for stateless HTTP) |
### Response gotchas
- `end()` can only be called **once**.
- `status()`/`header()`/`cookie()` must be called **before** the first `write()` or `end()`.
- `write()` switches to chunked transfer and disables compression.
### Task workers
```php
// Fire-and-forget
$server->task(['job' => 'resize', 'file' => $file]);
// Concurrent batch (requires task_enable_coroutine=true)
$results = $server->taskCo([['job' => 'a'], ['job' => 'b']], 5.0);
```
**Caveat**: `task()`/`taskwait()` only callable from event workers. Prefer `taskCo()` when `task_enable_coroutine=true`.
### WebSocket server
WS-specific methods:
```php
public function push(int $fd, Frame|string $data, int $opcode = WEBSOCKET_OPCODE_TEXT, int $flags = WEBSOCKET_FLAG_FIN): bool;
public function isEstablished(int $fd): bool; // NOT exists() -- use this for WS
public function disconnect(int $fd, int $code = WEBSOCKET_CLOSE_NORMAL, string $reason = ''): bool;
public function ping(int $fd, string $data = ''): bool;
```
Use `$server->isEstablished($fd)` (NOT `exists()`) to check WS connection. `onHandShake` gives full control of the handshake; `onOpen` fires after the built-in one.
**Broadcast pattern**:
```php
foreach ($server->connections as $fd) {
if ($server->isEstablished($fd)) $server->push($fd, $payload);
}
```
### TCP packet framing
`open_length_check` + `package_length_type` delivers complete packets to `onReceive`. EOF variant: `'open_eof_split' => true, 'package_eof' => "\r\n"`.
### Graceful reload
- `$server->reload()` -> workers finish in-flight, exit, get replaced.
- `reload_async=true`: old workers keep running current coroutines. `onWorkerExit` fires when done.
- **Smooth reload only picks up files `require`'d inside `onWorkerStart`**. Files required before `start()` stay cached forever.
- If `opcache.validate_timestamps=0`, add `opcache_reset()` at the top of `onWorkerStart`.
---
## 6. Process and Process\Pool
### `Swoole\Process`
```php
final class Swoole\Process {
public int $pipe;
public int $pid;
public int $id;
public function __construct(
callable $callback,
bool $redirectStdinStdout = false,
int $pipeType = SOCK_DGRAM, // 0=none, 1=SOCK_STREAM, 2=SOCK_DGRAM
bool $enableCoroutine = false
);
public function start(): bool|int;
public function write(string $data): int|false;
public function read(int $size = 8192): string|false;
// Sysv message queue IPC
public function useQueue(int $key = 0, int $mode = 2, int $capacity = -1): bool;
public function push(string $data): bool;
public function pop(int $size = 65536): string|false;
public function exportSocket(): Swoole\Coroutine\Socket|false;
public function name(string $name): bool;
public function exit(int $exitCode = 0): void;
public static function wait(bool $blocking = true): array|false;
public static function signal(int $signalNo, ?callable $callback = null): bool;
public static function kill(int $pid, int $signalNo = SIGTERM): bool;
public static function daemon(bool $nochdir = true, bool $noclose = true, array $pipes = []): bool;
}
```
`$enableCoroutine = true` makes the callback run inside a coroutine scheduler. Signal handling: use `Swoole\Process::signal()`, not `pcntl_signal()`.
```php
Swoole\Process::signal(SIGTERM, fn() => $server->shutdown());
Swoole\Process::signal(SIGINT, fn() => $server->shutdown());
```
### `Swoole\Process\Pool`
```php
final class Swoole\Process\Pool {
public function __construct(
int $workerNum,
int $ipcType = SWOOLE_IPC_NONE, // 0=none, 1=UNIXSOCK, 2=MSGQUEUE, 3=SOCKET
int $msgqueueKey = 0,
bool $enableCoroutine = false
);
public function set(array $settings): void;
public function on(string $event, callable $cb): bool;
public function getProcess(int $workerId = -1): Process|false;
public function listen(string $host, int $port = 0, int $backlog = 2048): bool;
public function write(string $data): bool;
public function sendMessage(string $data, int $dstWorkerId): bool;
public function start();
public function stop(): void;
public function shutdown(): bool;
}
```
Minimal supervised worker pool -- bring your own protocol, no reactor. Good for "run my callable across N processes with shared TCP socket". Events: `WorkerStart`, `WorkerStop`, `Message`, `Start`, `Shutdown`.
**Pool vs Server**: `Pool` has no `onConnect`/`onReceive`/HTTP/WS. `Server` is the full reactor + protocol helpers.
---
## 7. Shared memory -- Table, Atomic, Lock
### `Swoole\Table`
Mmap'd shared hash table -- the only way to share state across workers. Per-row spinlocks + CAS.
```php
final class Swoole\Table implements Iterator, Countable {
public const TYPE_INT = 1;
public const TYPE_FLOAT = 2;
public const TYPE_STRING = 3;
public function __construct(int $tableSize, float $conflictProportion = 0.2);
public function column(string $name, int $type, int $size = 0): bool;
public function create(): bool;
public function destroy(): bool;
public function set(string $key, array $value): bool;
public function get(string $key, ?string $field = null): mixed;
public function exists(string $key): bool;
public function del(string $key): bool;
public function count(): int;
public function incr(string $key, string $column, int|float $incrby = 1): int|float;
public function decr(string $key, string $column, int|float $incrby = 1): int|float;
public function getSize(): int;
public function getMemorySize(): int;
public function stats(): array|false;
}
```
```php
$table = new Swoole\Table(8192);
$table->column('name', Swoole\Table::TYPE_STRING, 64);
$table->column('age', Swoole\Table::TYPE_INT);
$table->column('score', Swoole\Table::TYPE_FLOAT);
$table->create(); // MUST create before $server->start()
$server->table = $table; // attach so workers can reach it
```
**Gotchas**:
- `TYPE_STRING` columns have fixed byte length; overflow silently truncated.
- Size ~30% over peak for collision chains.
- In 5.0+, `Table` **no longer implements `ArrayAccess`** -- use `set()`/`get()`.
### Atomic / Lock
`Swoole\Atomic` -- shared-memory counters (32-bit unsigned; `Atomic\Long` for 64-bit signed). `wait()`/`wakeup()` are futex-style and **block the entire process** -- use `Channel` in event workers instead.
`Swoole\Lock` -- **process-level, NOT coroutine-safe**. If a coroutine holds the lock and yields, deadlock. Use `Channel(1)` for coroutine-safe mutex. In 6.1+: API unified to `lock($op = LOCK_EX, $timeout = -1)`, `unlock()`. `lockwait()`/`trylock()` removed.
---
## 8. Coroutine clients
### `Swoole\Coroutine\Http\Client`
```php
new Swoole\Coroutine\Http\Client(string $host, int $port, bool $ssl = false)
// $host = IP, domain (async DNS), or unix://tmp/foo.sock. Do NOT pass http:// prefix.
```
**Properties**: `$errCode`, `$errMsg`, `$statusCode` (negative = network issue), `$body`, `$headers`, `$cookies`, `$set_cookie_headers`.
Negative statusCode constants: `-1` CONNECT_FAILED, `-2` REQUEST_TIMEOUT, `-3` SERVER_RESET, `-4` SEND_FAILED.
```php
set(array $options): void
setMethod(string $method): void
setHeaders(array $headers): void
setCookies(array $cookies): void
setData(string|array $data): void
addFile(string $path, string $name, ?string $mime = null, ?string $filename = null, int $offset = 0, int $length = 0): void
addData(string $data, string $name, ?string $mime = null, ?string $filename = null): void
get(string $path): bool
post(string $path, mixed $data): bool
download(string $path, string $filename, int $offset = 0): bool
upgrade(string $path): bool // websocket handshake
push(mixed $data, int $opcode = WEBSOCKET_OPCODE_TEXT, int $flags = WEBSOCKET_FLAG_FIN): bool
recv(float $timeout = 0): Frame|false
close(): bool
```
**Functional shortcuts** (`Swoole\Coroutine\Http` namespace, >= 4.6.4):
```php
use function Swoole\Coroutine\Http\{get, post, request};
$resp = get('https://httpbin.org/get?hello=world');
```
### `Swoole\Coroutine\Socket`
Low-level coroutine-native socket for custom protocols.
```php
new Swoole\Coroutine\Socket(int $domain, int $type, int $protocol);
bind(string $address, int $port = 0): bool
listen(int $backlog = 0): bool
accept(float $timeout = 0): Co\Socket|false
connect(string $host, int $port = 0, float $timeout = 0): bool
send(string $data, float $timeout = 0): int|false
sendAll(string $data, float $timeout = 0): int|false
recv(int $length = 65536, float $timeout = 0): string|false
recvAll(int $length, float $timeout = 0): string|false
recvPacket(float $timeout = 0): string|false // uses framing from setProtocol()
recvLine(int $length = 65536, float $timeout = 0): string|false
setProtocol(array $settings): bool // same framing options as Server->set()
checkLiveness(): bool
close(): bool
```
### Removed coroutine clients (6.0+)
**Removed entirely**: `Swoole\Coroutine\MySQL`, `Coroutine\Redis`, `Coroutine\PostgreSQL`. Use hooked PDO/ext-redis with `SWOOLE_HOOK_ALL`. Never generate code using these for 6.x.
---
## 9. Hooked PDO and curl
### Hooked PDO
With `SWOOLE_HOOK_ALL`, native `PDO` becomes coroutine-aware transparently.
**One-connection-per-coroutine rule** -- a single PDO handle is **not safe** across concurrent coroutines. The socket is bound to one coroutine; interleaved use corrupts the wire protocol. Use `PDOPool`.
**Persistent connections are NOT compatible** -- always `PDO::ATTR_PERSISTENT => false`.
**SQLite caveat**: `pdo_sqlite` hook requires SQLite in serialized/multi-threaded mode. Since 6.1.5, `PDO::sqliteCreateAggregate/Collation/Function` removed in coroutine mode.
### Hooked curl
**Use `SWOOLE_HOOK_NATIVE_CURL`** (included in `SWOOLE_HOOK_ALL`), not legacy `SWOOLE_HOOK_CURL`. **Guzzle and Symfony HttpClient work transparently**:
```php
Co\run(function () {
$guzzle = new \GuzzleHttp\Client();
$resp = $guzzle->get('https://httpbin.org/get'); // coroutine-aware, no changes needed
});
```
---
## 10. Connection pooling
`swoole/library` ships with ext-swoole (auto-loaded via `swoole.enable_library=On`).
### Canonical pattern -- "put it back or leak"
**A pool connection that isn't returned is lost forever.** Always `try/finally`:
```php
use Swoole\Database\{PDOConfig, PDOPool};
$pool = new PDOPool(
(new PDOConfig())
->withDriver('mysql')
->withHost('127.0.0.1')
->withPort(3306)
->withDbname('app')
->withCharset('utf8mb4')
->withUsername('app')
->withPassword('secret')
->withOptions([PDO::ATTR_EMULATE_PREPARES => false]),
size: 32,
);
go(function () use ($pool) {
$pdo = $pool->get();
try {
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE id = :id');
$stmt->execute(['id' => 1]);
} finally {
$pool->put($pdo); // MUST return, even on exception
}
});
// Or cleaner with defer:
go(function () use ($pool) {
$conn = $pool->get();
Co::defer(fn() => $pool->put($conn));
// ... use $conn ...
});
```
**Pool notes**:
- `get(-1)` blocks forever; `get($timeout)` returns `false` on timeout.
- `put(null)` signals broken connection -- pool decrements count, rebuilds next `get()`.
- `PDOProxy` auto-reconnects on lost-connection exceptions outside transactions.
- **Transactions must begin and commit on the same connection.** Don't return mid-transaction.
- `RedisPool` / `MysqliPool` follow the same shape.
- **SQLite restriction**: `PDOPool` rejects `:memory:` and empty DB names.
### Channel-as-pool pattern
```php
final class GrpcPool
{
private Channel $channel;
public function __construct(
private readonly \Closure $factory,
private readonly int $size = 32,
) {
$this->channel = new Channel($size);
for ($i = 0; $i < $size; $i++) {
$this->channel->push(($this->factory)());
}
}
public function get(float $timeout = -1): object
{
$conn = $this->channel->pop($timeout);
if ($conn === false) throw new \RuntimeException('pool exhausted or closed');
return $conn;
}
public function put(object $conn): void { $this->channel->push($conn); }
}
```
---
## 11. Pitfalls catalog
### Never block inside a coroutine
These stall the whole worker (not just one coroutine):
- `mysql`/`mysqli`/`pdo_mysql` **without** `mysqlnd + hooks`
- `sleep()` without `SWOOLE_HOOK_SLEEP`
- `curl_*` without `SWOOLE_HOOK_NATIVE_CURL`
- `file_*`/`fread`/`fwrite` without `SWOOLE_HOOK_FILE`
- Any extension bypassing PHP streams
**Fix**: `SWOOLE_HOOK_ALL` at bootstrap. Exclude flags only when a library fights the scheduler (e.g. `SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP` for PHPMailer raw SMTP).
### Sharing a connection across coroutines
Swoole error: `"redis client has already been bound to another coroutine"`. PDO/Redis/socket handles are bound to the first coroutine. **Always pool, one checkout per coroutine.**
**Multiple processes must not share connections either.** Create per-worker pools in `onWorkerStart`, not program-global.
### `pcntl_*` is forbidden
`pcntl_fork`/`pcntl_signal`/`pcntl_waitpid` conflict with Swoole's `signalfd`. Use `Swoole\Process`, `Process::signal()`, `Coroutine\System::waitSignal()`.
### `go()` in `onStart` / `onManagerStart`
`onStart` runs in the master. Coroutine APIs there can conflict with `dispatch_func` and `package_length_func`. Put bootstrap in **`onWorkerStart`**.
### Channel deadlocks
`Channel::pop()` with default `timeout = -1` hangs forever if all producers exited and channel is empty. **Always pass a timeout**, or pair with `$channel->close()`.
### Cooperative != concurrent-safe
Read-modify-write across an I/O boundary is a data race:
```php
// WRONG
$count = $table->get('k', 'count');
$count++; // yield possible if I/O runs
$table->set('k', ['count' => $count]);
// RIGHT
$table->incr('k', 'count');
```
### Exception handling across coroutines
Exceptions **cannot** cross coroutine boundaries. `go(fn() => throw new X)` inside an outer `try/catch` does nothing. Every top-level `go()` closure needs its own `try/catch`.
### Stashing `$this` in context
`Coroutine::getContext()['controller'] = $this` keeps the controller alive -- usually a leak. Stash only data.
### Framework compatibility
| Framework | Status |
|---|---|
| Hyperf | **Swoole-native**, production-ready |
| Webman | Fast, Swoole driver available |
| Imi | Swoole-native |
| Laravel Octane | Works, but use `app->scoped()` not `singleton()` for request-adjacent services |
| Slim / Mezzio | Work via PSR-15 bridges, not Swoole-aware |
| Phalcon | **Incompatible** -- C extension, globals-heavy |
---
## 12. Production tuning
### Worker sizing
| Setting | Guidance |
|---|---|
| `worker_num` | Default `swoole_cpu_num()`. Async/coroutine: 1-4x cores |
| `task_worker_num` | `(tasks/sec / tasks/worker/sec)`. Default 0 |
| `max_request` | Respawn after N requests. Mitigates leaks. **No effect in SWOOLE_BASE.** 10k-50k reasonable |
| `max_wait_time` | Drain timeout on reload/shutdown. 30-60s for HTTP |
| `reload_async` | **Set `true`** for graceful coroutine reload |
| `max_coroutine` | Per-worker cap, default 100000 |
### Buffers
| Setting | Default | Note |
|---|---|---|
| `package_max_length` | 2 MB | Max packet / HTTP POST body |
| `buffer_output_size` | 2 MB | Max `send()` payload. Memory = `worker_num x size` |
| `socket_buffer_size` | 2 MB | Per-connection send buffer |
### SSL / HTTP/2 / compression
- `ssl_cert_file` / `ssl_key_file`: PEM. Set `SWOOLE_SSL` socket flag.
- `open_http2_protocol`: requires `--enable-http2`.
- `http_compression`: gzip/brotli/zstd (6.0+ added zstd).
### Daemon / logs
- **`daemonize`**: `false` under systemd/Docker/k8s. Only `true` from interactive shell.
- **`log_file`**: use absolute paths (CWD changes after daemonize).
- `SIGRTMIN` reopens the log after logrotate.
- `log_rotation`: `SWOOLE_LOG_ROTATION_DAILY|HOURLY|MONTHLY`.
### Kernel tuning (Linux)
```
ulimit -n 262140
net.core.somaxconn = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 81920
net.core.wmem_max = 16777216
net.core.rmem_max = 16777216
net.unix.max_dgram_qlen = 100
```
Do **not** set `net.ipv4.tcp_tw_recycle = 1` -- removed in Linux 4.12, unsafe with NAT.
---
## 13. Testing
### PHPUnit entry point
```php
public function testAsync(): void
{
$result = null;
\Swoole\Coroutine\run(function () use (&$result) {
$client = new \Swoole\Coroutine\Http\Client('httpbin.org', 443, true);
$client->get('/ip');
$result = $client->body;
});
self::assertNotNull($result);
}
```
### Resetting state
Workers are resident -- static state persists. In `setUp()`: reset singletons, run each test in its own `Co\run` (fresh context), `Timer::clearAll()`. In `tearDown()` assert `Coroutine::stats()['coroutine_num'] === 0` -- non-zero means a leak.
### IDE stubs
```bash
composer require --dev swoole/ide-helper
```
PHPStan: add `vendor/swoole/ide-helper/src/swoole/constants.php` to `bootstrapFiles`.
---
## 14. Swoole 6.x version notes
### 6.0 (2024-12)
- **Removed**: `Coroutine\MySQL`, `Coroutine\Redis`, `Coroutine\PostgreSQL`. Use hooked PDO.
- Requires PHP 8.1+.
- **Multi-threading**: `SWOOLE_THREAD`, `Swoole\Thread`, `Thread\Map/ArrayList/Queue/Lock/Barrier/Atomic`. Requires ZTS + `--enable-swoole-thread`.
- **io_uring** for file AIO: `--enable-iouring`.
- `Swoole\Async\Client` (non-blocking TCP/UDP/Unix).
- zstd HTTP compression.
- Non-blocking reentrant coroutine mutex.
### 6.1 (2025-10)
- **llhttp** replaces `http_parser`.
- **Lock API unified**: `lock($op = LOCK_EX, $timeout = -1)`, `unlock()`. `lockwait()`/`trylock()` **removed**.
- Coroutine cancellation gains `$throwException` -> throws `CanceledException`.
- WebSocket: `disconnect()`, `ping()`, fragmented messages.
- Runtime hooks can **only** be set in main thread, before child threads.
- 6.1.5: `PDO::sqliteCreateAggregate/Collation/Function` removed from coroutine mode.
- 6.1.6: auto-strips `SWOOLE_HOOK_SOCKETS` if ext-sockets not loaded.
- 6.1.7: hooked `pdo_pgsql` gained timeout.
### 6.2 (2026-04)
- Requires **PHP 8.2+**. Supports up to PHP 8.5.
- Coroutine **FTP** (`--enable-swoole-ftp`) and **SSH** (`--with-swoole-ssh2`) clients.
- `SWOOLE_HOOK_PDO_FIREBIRD`, `SWOOLE_HOOK_MONGODB`, `SWOOLE_HOOK_NET_FUNCTION`.
- `Coroutine::setTimeLimit()` -- per-coroutine timeout.
- HTTP over io_uring sockets: `--enable-uring-socket`.
- `--enable-openssl` removed (always on). `liburing >= 2.8` required.
- `Coroutine::cancel()` cancels in-flight io_uring ops.
### Build flags cheatsheet
```bash
./configure --enable-swoole \
--enable-sockets \
--enable-swoole-thread \ # requires ZTS
--enable-swoole-curl \ # SWOOLE_HOOK_NATIVE_CURL
--enable-swoole-pgsql \
--enable-swoole-sqlite \
--with-swoole-firebird \ # 6.2+
--with-swoole-ssh2 \ # 6.2+
--enable-swoole-ftp \ # 6.2+
--enable-iouring \ # Linux io_uring
--enable-uring-socket \ # 6.2+
--enable-brotli \
--enable-zstd \ # 6.0+
--enable-cares # c-ares DNS
```
### swoole/library version alignment
`swoole/library` is bundled into ext-swoole and auto-loaded. Composer releases are tagged for 6.1 and 6.2 (latest v6.2.3 as of 2026-09), so `composer require swoole/library` resolves to 6.2.x. Treat the extension as source of truth for library classes in production.
---
## Quick reference -- canonical program skeleton
```php
<?php
declare(strict_types=1);
use Swoole\Coroutine;
use Swoole\Coroutine\{Barrier, Channel};
use function Swoole\Coroutine\run;
Coroutine::set([
'hook_flags' => SWOOLE_HOOK_ALL,
'max_coroutine' => 10_000,
'socket_timeout' => 5,
'enable_deadlock_check' => true,
]);
run(function (): void {
$jobs = new Channel(32);
$results = new Channel(32);
$barrier = Barrier::make();
for ($w = 0; $w < 4; $w++) {
go(function () use ($jobs, $results, $barrier): void {
while (($job = $jobs->pop()) !== false) {
$results->push(['job' => $job, 'pid' => getmypid()]);
}
});
}
go(function () use ($jobs, $barrier): void {
foreach (range(1, 20) as $i) $jobs->push($i);
$jobs->close();
});
go(function () use ($results, $barrier): void {
defer(fn() => $results->close());
$collected = [];
for ($i = 0; $i < 20; $i++) {
$msg = $results->pop(5.0);
if ($msg === false) break;
$collected[] = $msg;
}
var_dump($collected);
});
Barrier::wait($barrier);
});
```