Xdebug setup for step-debugging and profiling
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
# 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 tapsAfter installation, add to your php.ini (or better, create /etc/php/8.4/mods-available/xdebug.ini):
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003Xdebug modes
The xdebug.mode directive controls everything:
| Mode | Purpose |
|---|---|
off | Xdebug loaded but disabled (zero overhead) |
debug | Step debugging via DAP (Debug Adapter Protocol) |
coverage | Code coverage data collection (for PHPUnit --coverage-*) |
profile | Profiling — generates cachegrind files |
trace | Function call tracing — logs every function call |
develop | Enhanced var_dump(), error display improvements |
Multiple modes: xdebug.mode=debug,develop.
Step debugging workflow
- IDE (PhpStorm, VS Code with PHP Debug extension) listens on port 9003
- Set
xdebug.start_with_request=yes(trigger on every request) ortrigger(only whenXDEBUG_SESSIONcookie/query param is present) - Set a breakpoint in your IDE
- Make the HTTP request or run
php artisan - 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
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.