Debug OpenSwoole Coroutines with Xdebug Step Debugging

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.

The Old Problem

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:

  • Breakpoints inside coroutines wouldn't trigger
  • Step-over would jump to a completely different coroutine
  • Variable inspection showed stale data from a previous context
  • Stack traces mixed frames from different coroutines
  • On some setups, the process would crash outright

The practical effect was that most OpenSwoole developers debugged with var_dump() and error_log(). Not ideal.

How Fiber Context Fixes It

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

Setting Up Xdebug with OpenSwoole

1. Install Xdebug

pecl install xdebug

2. Configure php.ini

[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.

3. Configure Your IDE

PhpStorm:

  1. Go to Settings > PHP > Debug
  2. Set Xdebug port to 9003
  3. Click "Start Listening for PHP Debug Connections" (the phone icon)
  4. Set breakpoints in your code

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}"
            }
        }
    ]
}

4. Start Your Server

<?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.

Debugging Tips for OpenSwoole

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.

Profiling with Blackfire and Tideways

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.

Performance Overhead

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',
]);

Requirements

  • OpenSwoole 26.2.0 or later
  • PHP 8.3 or later (Fibers were introduced in 8.1, but fiber context requires 8.3+)
  • Xdebug 3.x for step debugging

Install OpenSwoole 26.2.0

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.