Guide
How to use Xdebug locally on Mac.
Step debugging beats var_dump() once it works, and the setup is mostly the same four settings on every Mac. This guide covers the Homebrew way for any local PHP setup, the PhpStorm and VS Code side, and the performance cost of leaving it on. At the end there's the shortcut in Bothy, my native Mac app for local PHP sites, where Xdebug is a per-site switch.
Install Xdebug for each PHP version you use, set xdebug.mode=debug with the client on port 9003, prefer trigger mode over debugging every request, and point PhpStorm or VS Code at port 9003. Turn it off when you're not stepping through code, because it slows every request.
How Xdebug step debugging works
Xdebug is a PHP extension. When a request starts a debug session, Xdebug opens a connection from PHP to your editor using the DBGp protocol. Your editor is the server here: it listens on a port, PHP connects to it, and then the editor tells PHP where to pause.
That direction matters for troubleshooting. If breakpoints never hit, the question is almost always "can PHP reach the editor on the port it expects?" rather than anything about the web server.
Xdebug 3 simplified configuration to a handful of settings:
xdebug.mode- which features are on:debugfor step debugging,developfor nicer error output andvar_dump(),coverage,profile,trace, oroff. Combine with commas.xdebug.client_hostandxdebug.client_port- where your editor is listening. The Xdebug 3 default port is 9003 (Xdebug 2 used 9000, which clashed with php-fpm).xdebug.start_with_request-yesto try to debug every request, ortriggerto debug only when a request asks for it.
Installing Xdebug for each PHP version
Xdebug is compiled against a specific PHP version, so every PHP you have installed needs its own copy. With Homebrew PHP there are two common routes.
- 1Check which PHP you're actually running
The CLI and your web server can use different PHP builds. Check both:
php -vin the terminal, andphpinfo()in a page served by your local setup. If you juggle versions, see running multiple PHP versions on a Mac.php -v php --ini # which php.ini files are loaded
- 2Option A: PECL
PECL builds Xdebug against whichever
phpandphpizecome first on your PATH, so run it once per version, switching PATH (or calling that version'specldirectly) each time. With Homebrew's own formulae it usually adds thezend_extensionline for you.pecl install xdebug php -v # should now mention "with Xdebug"
- 3Option B: the shivammathur extensions tap
If your PHP comes from the
shivammathur/phptap, its companion tap has prebuilt formulae per version, which avoids compiling. Install the one matching each PHP you use.brew tap shivammathur/extensions brew install shivammathur/extensions/xdebug@8.3 brew install shivammathur/extensions/xdebug@8.2
- 4Configure it
Put the settings in that version's php.ini, or a separate
ext-xdebug.iniin itsconf.ddirectory (php --inishows where). Start with trigger mode; you can switch toyeslater.zend_extension="xdebug.so" xdebug.mode=debug,develop xdebug.client_host=127.0.0.1 xdebug.client_port=9003 xdebug.start_with_request=trigger
- 5Restart PHP-FPM and confirm
The CLI picks up changes immediately; php-fpm needs a restart. Then confirm in
phpinfo()that the Xdebug section shows the mode you set.xdebug_info()prints a diagnostic page that also shows whether the last connection attempt worked.brew services restart php@8.3 php -r 'xdebug_info();' | head -40
start_with_request: yes or trigger?
With start_with_request=yes, every request tries to connect to your editor. That's convenient while you're actively debugging, but when the editor isn't listening each request still makes the attempt, and Xdebug may write "Could not connect to debugging client" warnings to your log.
With trigger, a session starts only when the request carries an XDEBUG_TRIGGER (or the older XDEBUG_SESSION) cookie, GET/POST parameter or environment variable. Ways to send it:
- A browser extension such as Xdebug Helper, which sets the cookie with one click.
- A query string for one-off requests:
https://shop.test/checkout?XDEBUG_TRIGGER=1 - An environment variable for CLI scripts, artisan commands and tests.
Debugging CLI scripts and tests
Xdebug works the same for command-line PHP. Set the trigger in the environment; PhpStorm also needs to know which server mapping to use.
XDEBUG_TRIGGER=1 php artisan queue:work --once XDEBUG_TRIGGER=1 vendor/bin/phpunit --filter CheckoutTest # PhpStorm only: tell it which server/path mapping applies PHP_IDE_CONFIG="serverName=shop.test" XDEBUG_TRIGGER=1 php bin/console app:import
Replace shop.test with the server name you set up in your IDE.
Setting up PhpStorm
- Open Settings → PHP → Debug. Under Xdebug, set the debug port to
9003(you can list9000,9003if some old project still uses 9000). - Click Start Listening for PHP Debug Connections in the toolbar (the phone-handset icon).
- Set a breakpoint and load the page with the trigger set. The first time, PhpStorm asks which local project the incoming connection belongs to and creates a server entry named after the host (for example
shop.test). - Because your code runs from the same folder PhpStorm has open, no path mappings are needed. You only need them for Docker, VMs or symlinked projects.
PhpStorm's Run → Web Server Debug Validation checks a running site's Xdebug settings and is worth a click when nothing happens.
Setting up VS Code
Install the PHP Debug extension (publisher: Xdebug), then add a launch configuration. Locally, with the project opened from the folder the site is served from, this is all you need:
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003
}
]
}.vscode/launch.json - start it with F5, then trigger a request. Add "pathMappings" only if the served path differs from the folder you have open.
The performance cost of leaving Xdebug on
Xdebug hooks into the engine to do its job, and that isn't free. Roughly, from cheapest to most expensive:
- Not loaded at all - no cost.
- Loaded with
xdebug.mode=off- small but measurable overhead, because the extension is still in the process. debugwithtrigger- requests without a trigger are only slightly slower; triggered ones are as slow as you make them by stepping.debugwithstart_with_request=yes- every request attempts a connection; with no editor listening, each attempt waits forxdebug.connect_timeout_msbefore carrying on.develop,coverage,profile,trace- noticeably slower, sometimes several times over, especially on large frameworks and test suites.
Composer warns when it runs with Xdebug loaded for the same reason. The practical rule: keep Xdebug off on sites you're not debugging, and switch it on only for the one you are.
Common pitfalls
- Breakpoints never hit. Is the editor actually listening, and on 9003? Is the trigger being sent? Check
xdebug_info()for the last connection error. - Installed for the wrong PHP.
php -vin the terminal says "with Xdebug" but the site doesn't: the web server uses a different version or a php-fpm that wasn't restarted. - Port 9003 already taken. Two editors listening at once (PhpStorm and VS Code) fight over the port. Only one should listen.
- Still on port 9000. Old tutorials and IDE settings use Xdebug 2's port. With Xdebug 3, the default is 9003.
- Xdebug 2 settings in php.ini.
xdebug.remote_enableandxdebug.remote_portno longer do anything; Xdebug 3 logs a warning about them. - Everything feels slow. Xdebug is loaded on every site and every version. Turn it off where you don't need it.
Xdebug in Bothy: one switch per site
If you use Bothy, the installing and ini-editing above is handled for you. Xdebug is a toggle on each site's page in the app, or one command.
- 1Turn it on for the site you're debugging
Bothy installs Xdebug for that site's PHP version on first use. It connects to 127.0.0.1:9003 in
debug,developmode, withstart_with_request=yes, so every request to that site starts a debug session (no browser extension or trigger needed) and the PhpStorm and VS Code settings above work unchanged. Other sites stay atxdebug.mode=off.bothy site xdebug shop on
- 2Debug CLI commands with the same PHP
bothy site execruns in the site folder with that site's PHP first on PATH, so artisan, wp and phpunit run under the version you're debugging.bothy site exec shop -- env XDEBUG_TRIGGER=1 php artisan migrate:status
- 3Turn it off when you're done
The rest of your sites carry on without it, so only the site you're working on pays the cost.
bothy site xdebug shop off
In Bothy, Xdebug is bothy site xdebug shop on, or the bothy_set_site_xdebug tool if you'd rather ask Claude or another MCP client to do it. See Xdebug in Bothy and the Sites docs. Bothy is $69, one-off.
Questions
9003 by default. Xdebug 2 used 9000, which clashed with php-fpm's default port, so old tutorials and IDE settings often still say 9000.
Yes. Xdebug is compiled for a specific PHP version, so PHP 8.2 and 8.3 each need their own copy and their own ini settings.
Trigger is the better default: only requests carrying XDEBUG_TRIGGER start a session, so ordinary page loads don't try to reach your editor. Use yes for short periods when you want every request to stop at breakpoints.
Yes. Just having the extension loaded adds some overhead, and modes like develop, coverage and profile add a lot more. Switch it off on sites you're not debugging.
Usually the editor isn't listening, it's on the wrong port, the trigger isn't set, or Xdebug is installed for a different PHP than the one serving the site. xdebug_info() shows the last connection attempt and its error.
Yes. php -S loads the same php.ini as the CLI, so if php -v shows Xdebug, the built-in server has it too.