Xdebug in production: we measured it
40 ms became 1012 ms, and sixty requests wrote 1.2 GB of cachegrind files. Here is the method, the raw numbers, and where our own tool costs the most.
“Just turn on Xdebug's profiler for a minute on production” is advice that circulates in every PHP team. We wanted a number instead of a feeling, so we measured it.
To be clear up front: Xdebug is excellent at the job it was built for. Step debugging, local profiling, code coverage — nothing else comes close, and its documentation never claims the profiler belongs on a live server. This article is about what happens when you try anyway, because people do.
The method
One container, PHP 8.3.33 on Apache, one page doing 720,000 user function calls —
the shape of a template-heavy request. The page times itself with microtime(),
so the numbers are PHP work, not HTTP overhead. Twelve requests per measurement, median
taken. Variants were interleaved across five rounds rather than run in
blocks, so drift in the machine cannot favour one of them.
# the page under test, in short function pomocnik($x) { return $x % 7 === 0 ? $x * 2 : $x + 1; } function warstwa($n) { $s = 0; for ($i = 0; $i < $n; $i++) { $s += pomocnik($i); } return $s; } function render($n) { $out = 0; for ($i = 0; $i < 12; $i++) { $out += warstwa($n); } return $out; } render(60000); # 720,000 calls
The numbers
| Configuration | Median | vs baseline |
|---|---|---|
| Nothing loaded | 40.2 ms | — |
| PHPRay, request recording only | 40.7 ms | within noise |
| PHPRay, with per-function attribution | 51.0 ms | +27% |
Xdebug, xdebug.mode=profile | 1012 ms | 25× |
Five interleaved rounds, spread of the Xdebug figure 999–1034 ms, spread of the baseline 39.5–40.8 ms. The gap is not subtle enough to need statistics.
The second cost is storage. Sixty requests produced 30 cachegrind files totalling 1.2 GB — roughly 40 MB per profiled request. On a site doing a few requests per second, the disk is gone before you have finished reading the first file.
Where our own tool costs the most
The inconvenient row in our own table
Per-function attribution cost us 27% on this page. That is our worst case and we are publishing it rather than quoting only the flattering number: this page is 720,000 user function calls, which is precisely what a function observer has to pay for. On a real WordPress page rendering in 48 ms we measured about 4% (method and raw numbers).
The row above it is the one that matters for “can I leave this on”: recording every request — time, SQL, outbound HTTP, errors, N+1 — came out at 40.7 ms against a 40.2 ms baseline. Indistinguishable, on the page built to be hostile to it.
So what do you actually use?
Honestly, it depends on what you can install and what you are willing to pay.
- Blackfire, Tideways, New Relic, Datadog — real products that solve production profiling well. They need an agent installed on the host and are priced per host. On shared hosting you cannot install the agent at all.
- Xdebug — locally, for the deep question. Not on the live server, as the table shows.
- PHPRay — a C extension plus one Go binary, Apache-2.0 core, no
agent with root required: your own
php.iniis enough on most shared hosting. Records every request, keeps the history, and gives per-plugin attribution on a sample when you turn it on.
We build the third one, so read the table rather than the sentence. The point of the measurement is that the first row and the last row are not competing for the same slot: one is a profiler you open when you already know which request to look at, the other is the recording that tells you which request that is.
Read next
- What always-on tracing costsWhy sampling does not make the function observer cheaper, measured on WordPress.
- A PHP profiler without rootYour own php.ini is enough on most shared hosting. Verified as an unprivileged user.
- All postsMeasurements with the method attached.
Look at your own traffic
Open source, Apache-2.0, no account needed for the local install.
How it works in production → See a real report