0

Xdebug setup for step-debugging and profiling

Beginner5 min read·php-01-005
performance

Concept

Xdebug is the debugging and profiling extension for PHP. It enables three distinct capabilities that every PHP developer needs: step debugging (pausing execution at breakpoints and inspecting variables), code coverage (measuring which lines tests execute), and profiling (generating performance callgraphs to identify bottlenecks).

Xdebug 3.x (the current generation) overhauled the configuration model. The old xdebug.remote_* settings are gone — everything is now controlled by xdebug.mode.

Installation

bash
# Via PECL (all platforms)
pecl install xdebug

# Ubuntu/Debian
apt-get install php8.4-xdebug

# macOS Homebrew
pecl install xdebug
# or: brew install php@8.4 already includes it in some taps

After installation, add to your php.ini (or better, create /etc/php/8.4/mods-available/xdebug.ini):

ini
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003

Xdebug modes

The xdebug.mode directive controls everything:

ModePurpose
offXdebug loaded but disabled (zero overhead)
debugStep debugging via DAP (Debug Adapter Protocol)
coverageCode coverage data collection (for PHPUnit --coverage-*)
profileProfiling — generates cachegrind files
traceFunction call tracing — logs every function call
developEnhanced var_dump(), error display improvements

Multiple modes: xdebug.mode=debug,develop.

Step debugging workflow

  1. IDE (PhpStorm, VS Code with PHP Debug extension) listens on port 9003
  2. Set xdebug.start_with_request=yes (trigger on every request) or trigger (only when XDEBUG_SESSION cookie/query param is present)
  3. Set a breakpoint in your IDE
  4. Make the HTTP request or run php artisan
  5. IDE pauses at the breakpoint — inspect $_GET, $this, local variables, call stack

Profiling workflow

Set xdebug.mode=profile and xdebug.output_dir=/tmp/profiles. Xdebug generates cachegrind.out.* files. Open them in:

  • KCacheGrind (Linux/KDE)
  • QCacheGrind (macOS)
  • Webgrind (browser-based, PHP)

These show you exactly how much time each function spent and how many times it was called. This is how you find that one database query called 500 times in a loop.

Performance impact

Xdebug adds 2–5x overhead even in off mode if the extension is loaded. Never load Xdebug on production servers. Use php_uname('n') or environment checks to conditionally enable it only in development Docker containers or local environments.

Code Example

php
<?php
declare(strict_types=1);

// Example: testing whether Xdebug is available and its current mode
if (extension_loaded('xdebug')) {
    $mode = ini_get('xdebug.mode');
    echo "Xdebug loaded. Mode: {$mode}\n";

    // xdebug_info() prints full diagnostics (HTML by default in web context)
    // xdebug_get_code_coverage() returns coverage data when mode=coverage
} else {
    echo "Xdebug not loaded. Good (production) or install it (development).\n";
}

// Practical: wrapping expensive operations to measure them
function measureTime(callable $fn): array
{
    $start = hrtime(true); // nanoseconds, monotonic clock
    $result = $fn();
    $elapsed = hrtime(true) - $start;

    return [
        'result'      => $result,
        'elapsed_ms'  => round($elapsed / 1_000_000, 3),
    ];
}

$measurement = measureTime(function () {
    // Simulate work
    $sum = 0;
    for ($i = 0; $i < 1_000_000; $i++) {
        $sum += $i;
    }
    return $sum;
});

echo "Result: {$measurement['result']}, Time: {$measurement['elapsed_ms']}ms\n";

// A .vscode/launch.json for VS Code + PHP Debug extension:
/*
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003
        },
        {
            "name": "Launch artisan",
            "type": "php",
            "request": "launch",
            "program": "${workspaceFolder}/artisan",
            "args": ["route:list"],
            "cwd": "${workspaceFolder}"
        }
    ]
}
*/

Interview Q&A

Q: What is the difference between Xdebug's debug, profile, and coverage modes?

debug mode implements the Debug Adapter Protocol (DAP), allowing IDEs to set breakpoints, pause execution, and inspect variables in real time. It has moderate overhead because it must check breakpoints at each opcode. profile mode instruments every function call and records how long each takes, writing a cachegrind-format output file — useful for finding bottlenecks post-hoc, not for interactive debugging. coverage mode tracks which lines of code are actually executed during a request or test run, used by PHPUnit to generate HTML coverage reports. You never want debug and profile active simultaneously; the debugger pauses distort timing measurements. In CI, only coverage mode is used and only during the test suite.


Q: How do you use Xdebug without slowing down every request?

Use xdebug.start_with_request=trigger instead of yes. In trigger mode, Xdebug only activates the debugging session when it detects a specific trigger: the XDEBUG_SESSION cookie (set by browser extensions like Xdebug Helper for Chrome), the XDEBUG_SESSION_START query parameter, or the XDEBUG_SESSION environment variable. Normal requests have zero Xdebug overhead; only the requests you intentionally trigger are debugged. For CLI debugging with trigger mode, set XDEBUG_SESSION=1 php artisan your:command.


Q: Why should you never run Xdebug in production and what are the alternatives for production debugging?

Xdebug adds overhead even when not actively debugging — the extension hooks into the Zend VM's execution loop to check for breakpoints. In profiling/coverage mode the overhead is even higher (5–10x slowdown). More critically, xdebug.start_with_request=yes would expose your internal application state to anyone who could reach port 9003, which is a serious security vulnerability. For production visibility use: Blackfire.io (low-overhead continuous profiling, SaaS), Sentry (error tracking with stack traces), Laravel Telescope (query/request/job inspector), or New Relic/Datadog APM. These tools are designed for production workloads and don't expose debugging interfaces.