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.

In short

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: debug for step debugging, develop for nicer error output and var_dump(), coverage, profile, trace, or off. Combine with commas.
  • xdebug.client_host and xdebug.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 - yes to try to debug every request, or trigger to 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.

  1. 1
    Check which PHP you're actually running

    The CLI and your web server can use different PHP builds. Check both: php -v in the terminal, and phpinfo() 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
  2. 2
    Option A: PECL

    PECL builds Xdebug against whichever php and phpize come first on your PATH, so run it once per version, switching PATH (or calling that version's pecl directly) each time. With Homebrew's own formulae it usually adds the zend_extension line for you.

    pecl install xdebug
    php -v             # should now mention "with Xdebug"
  3. 3
    Option B: the shivammathur extensions tap

    If your PHP comes from the shivammathur/php tap, 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
  4. 4
    Configure it

    Put the settings in that version's php.ini, or a separate ext-xdebug.ini in its conf.d directory (php --ini shows where). Start with trigger mode; you can switch to yes later.

    zend_extension="xdebug.so"
    xdebug.mode=debug,develop
    xdebug.client_host=127.0.0.1
    xdebug.client_port=9003
    xdebug.start_with_request=trigger
  5. 5
    Restart 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

  1. Open Settings → PHP → Debug. Under Xdebug, set the debug port to 9003 (you can list 9000,9003 if some old project still uses 9000).
  2. Click Start Listening for PHP Debug Connections in the toolbar (the phone-handset icon).
  3. 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).
  4. 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.
  • debug with trigger - requests without a trigger are only slightly slower; triggered ones are as slow as you make them by stepping.
  • debug with start_with_request=yes - every request attempts a connection; with no editor listening, each attempt waits for xdebug.connect_timeout_ms before 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 -v in 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_enable and xdebug.remote_port no 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.

  1. 1
    Turn 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,develop mode, with start_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 at xdebug.mode=off.

    bothy site xdebug shop on
  2. 2
    Debug CLI commands with the same PHP

    bothy site exec runs 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
  3. 3
    Turn 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

What port does Xdebug 3 use?

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.

Do I need to install Xdebug separately for each PHP version?

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.

Should I use start_with_request=yes or trigger?

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.

Does Xdebug slow PHP down even when I'm not debugging?

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.

Why don't my breakpoints hit?

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.

Can I use Xdebug with PHP's built-in server?

Yes. php -S loads the same php.ini as the CLI, so if php -v shows Xdebug, the built-in server has it too.

Get Bothy for $69.
One payment for every 1.x release. Version 2 will be a separate purchase.
Buy now