Debugging OpenSwoole applications with Xdebug used to range from "doesn't work" to "crashes the process." OpenSwoole's coroutines ran on custom context backends (Boost ASM, ucontext) that Xdebug knew nothing about. You'd hit a breakpoint, step into a function that triggers a coroutine switch, and Xdebug would lose track of where it was. You might get wrong stack traces, missed breakpoints, or the dreaded "extremely dangerous" warning followed by a segfault.
OpenSwoole 26.2.0 introduced fiber context mode, which changes the coroutine backend to use PHP's native zend_fiber API. Since Xdebug already understands Fibers, step debugging just works.
OpenSwoole coroutines are cooperative. When a coroutine hits an I/O operation — a database query, an HTTP request, a Co::sleep() — it yields control so another coroutine can run. The switching happened at the C level using Boost ASM or ucontext, completely outside PHP's execution model.
Xdebug hooks into PHP's execution engine to track function calls, variable scopes, and stack frames. When OpenSwoole swapped execution contexts underneath it, Xdebug's internal state got out of sync. The result:
The practical effect was that most OpenSwoole developers debugged with var_dump() and error_log(). Not ideal.
PHP 8.1 introduced Fibers. Xdebug 3.x added support for them. When you suspend and resume a Fiber, Xdebug correctly saves and restores its debugging state for that Fiber.
OpenSwoole 26.2.0's fiber context mode runs each coroutine inside a native PHP Fiber. From Xdebug's perspective, a coroutine switch is just a Fiber suspend/resume — something it already handles. No special integration needed.
Enable it with one setting:
<?php
OpenSwoole\Coroutine::set([
'use_fiber_context' => true,
]);
Or in php.ini:
openswoole.use_fiber_context=On
pecl install xdebug
[xdebug]
xdebug.mode=debug
xdebug.start_with_request=trigger
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
[openswoole]
openswoole.use_fiber_context=On
Setting start_with_request=trigger means Xdebug only activates when you send the XDEBUG_TRIGGER cookie or query parameter. This avoids the overhead of debugging every request — important in a long-running server where you don't want to slow down all workers permanently.
PhpStorm:
VS Code (with PHP Debug extension):
Add this to .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www": "${workspaceFolder}"
}
}
]
}
<?php
use OpenSwoole\Coroutine;
use OpenSwoole\Http\Server;
use OpenSwoole\Http\Request;
use OpenSwoole\Http\Response;
Coroutine::set([
'use_fiber_context' => true,
'hook_flags' => OpenSwoole\Runtime::HOOK_ALL,
]);
$server = new Server("127.0.0.1", 9501);
$server->set(['worker_num' => 1]); // Use 1 worker for debugging
$server->on("request", function (Request $request, Response $response) {
$userId = $request->get['id'] ?? 1;
// Set a breakpoint here — it will trigger correctly
$user = fetchUser((int)$userId);
$orders = fetchOrders((int)$userId);
$response->header('Content-Type', 'application/json');
$response->end(json_encode([
'user' => $user,
'orders' => $orders,
]));
});
function fetchUser(int $id): array
{
// Xdebug tracks execution correctly across this coroutine switch
$pdo = new PDO('mysql:host=127.0.0.1;dbname=app', 'root', '');
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
}
function fetchOrders(int $id): array
{
$pdo = new PDO('mysql:host=127.0.0.1;dbname=app', 'root', '');
$stmt = $pdo->prepare("SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 10");
$stmt->execute([$id]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$server->start();
Start the server:
php server.php
Trigger a debug session:
curl "http://127.0.0.1:9501/?id=42&XDEBUG_TRIGGER=1"
Your IDE should break at the breakpoint. You can step into fetchUser(), and when the PDO query triggers a coroutine switch, Xdebug stays on track. Step-over works. Variable inspection shows the right values. Stack traces are correct.
Use a single worker for debugging. Set worker_num => 1 so all requests hit the same process. With multiple workers, your debug session might connect to a worker that isn't handling the request you triggered.
Use start_with_request=trigger instead of yes. OpenSwoole servers are long-running. With start_with_request=yes, Xdebug tries to connect for every request on every worker from the moment the server starts. This slows things down and floods your IDE with connections you don't care about.
Breakpoints in callbacks work. You can set breakpoints inside on("request"), on("message"), timer callbacks, and task worker callbacks. They all run inside coroutines that use fiber context.
Watch out for concurrent requests. If two requests come in while you're paused at a breakpoint, the second request blocks until the first coroutine finishes or resumes. This is expected — coroutines are cooperative, and the event loop can't process new events while a coroutine is paused in the debugger.
Fiber context also fixes profiling. Blackfire and Tideways hook into PHP's execution engine the same way Xdebug does, so they had the same problems with OpenSwoole's custom context backends.
With fiber context enabled, profilers correctly attribute time and memory to the right coroutine. A profile of an HTTP request handler shows the actual call tree for that request, not a jumbled mix of frames from unrelated coroutines.
; For Blackfire profiling
openswoole.use_fiber_context=On
No changes to Blackfire's configuration — it just works because it already supports Fibers.
Fiber context adds a small overhead compared to Boost ASM. In benchmarks, the difference is around 2-5% on context switch microbenchmarks. In real applications with actual I/O, the difference is negligible — your database queries and HTTP calls take orders of magnitude longer than the context switch itself.
For production, you can leave fiber context off if you don't need Xdebug or profiler support. For development and staging, turn it on. A reasonable setup:
; php.ini for development
openswoole.use_fiber_context=On
; php.ini for production
; openswoole.use_fiber_context=Off (default)
Or set it conditionally in your bootstrap:
<?php
Coroutine::set([
'use_fiber_context' => getenv('APP_DEBUG') === 'true',
]);
The easiest way to install OpenSwoole is via PIE (PHP Installer for Extensions):
pie install openswoole/ext-openswoole
Or via PECL:
pecl install openswoole-26.2.0
Install the core library:
composer require openswoole/core:26.2.0
Docker images are also available:
docker pull openswoole/openswoole:26.2-php8.5-alpine
For the full fiber context API, see the Coroutine Fiber Context documentation. For installation help, see the installation documentation.
Join 4,000+ others and never miss out on new tips, tutorials, and more.