Swoole's Coroutine Model Explained: Cooperative Scheduling, Resident-Memory Traps, and How It Compares to Go's goroutines
PHP developers moving from FPM to Swoole gain huge concurrency but inherit a new class of bugs: stale static state, connection reuse failures, and coroutine leaks that only surface under production load. Knowing where cooperative scheduling and resident memory diverge from both FPM and Go prevents the kind of silent production incidents that are hard to reproduce locally.
PHP's classic FPM model gives each request its own process, then tears everything down when the request ends. That makes concurrency expensive and prevents connection reuse. Swoole's coroutines let PHP keep the synchronous code style while getting async performance: many coroutines run on one thread inside a Worker process, switching in user space on I/O operations at a fraction of a thread switch's cost.
The article walks through the practical consequences of resident memory. Static variables and global state survive across requests, so request data must live in coroutine context instead of `static` fields. Connections in a pool can silently die server-side; they need health checks on borrow and return. Coroutines can also leak when they hang forever on a Channel `pop` without timeout, and tracking `coroutine_num` plus dumping call stacks with `Co::getBackTrace()` is the reliable way to find them.
A key contrast runs throughout: Swoole coroutines are cooperative and single-threaded, so a CPU-bound loop blocks the whole Worker, and parallelism only comes from running more Worker processes. Go's goroutines are preemptively scheduled on multiple threads, making them a better fit for mixed CPU-bound workloads. Swoole's real strength is I/O-bound concurrency at scale.
The article's central comparison makes the design trade-off explicit: Swoole optimizes for I/O-bound concurrency by accepting a cooperative scheduler with no intra-worker parallelism, while Go accepts preemption overhead to protect against CPU-bound starvation.
The static-variable contamination example is the strongest argument for discipline in resident processes: a pattern that is harmless in FPM becomes a correctness bug the moment the process outlives a single request.
The coroutine leak diagnosis walkthrough highlights that the hard part is not detecting a leak but attributing it to a specific line; `Co::getBackTrace()` fills a real gap that memory profiling alone cannot.
The connection pool health-check requirement shows that Swoole moves the burden of connection lifecycle management onto the application, something FPM developers never had to think about.
The guidance to set timeouts on every blocking operation and use WaitGroup for every `go()` reads as a checklist for disciplined shutdown, which is a common source of subtle production issues in long-running services.