Xdebug Trace — Record Every Function Call
An Xdebug function trace lists every function call of one PHP request: what was called, from which file and line, how long it took and how much memory it used. You record the trace with Xdebug and send the file to Buggregator. Buggregator stores it, sums it up and lets you walk the calls as a tree and search them.
Example: a page takes two seconds. The trace shows that 1.6 seconds of it went to one repository method, called 400 times in a loop.
Available since server v2.8.0. You send the file over HTTP. Buggregator does not pick up trace files from a folder by itself.
Record a trace
Turn on tracing in php.ini (or with -d flags, see below):
xdebug.mode=trace
xdebug.trace_format=1
xdebug.start_with_request=trigger
xdebug.output_dir=/tmp/tracesxdebug.mode=traceturns tracing on. You can combine it with other modes, e.g.xdebug.mode=debug,trace.xdebug.trace_format=1is required. It is the computer-readable format. Buggregator refuses the other formats (0,2,3,4) with400.xdebug.start_with_requestsays which requests are traced.triggertraces only a request with theXDEBUG_TRIGGERcookie, query or POST parameter (or env variable for a CLI script).yestraces every request.xdebug.output_diris where Xdebug writes the files. The default is/tmp.
Optional:
xdebug.collect_return=1adds return values.xdebug.collect_assignments=1adds variable assignments.xdebug.use_compression(Xdebug 3.1+, on when Xdebug has gzip support) writes.xt.gzfiles. Send them as they are: Buggregator reads gzip itself.
Arguments are in the trace by default; xdebug.collect_params=0 leaves them out. Buggregator reads both.
For one CLI script, pass the settings as flags:
php -d xdebug.mode=trace -d xdebug.trace_format=1 -d xdebug.start_with_request=yes \
-d xdebug.output_dir=/tmp/traces bin/console app:importSend the trace to Buggregator
Post the file as the request body:
curl -H 'X-Buggregator-Event: xdebug-trace' \
--data-binary @/tmp/traces/trace.1234567890.xt.gz \
http://127.0.0.1:8000/Buggregator answers 200 with {"status":true} and stores the trace as an event. The answer has no event id.
Instead of the header, you can put the type in the address: http://xdebug-trace@127.0.0.1:8000/.
Optional headers:
| Header | What it does |
|---|---|
X-Buggregator-Project | Puts the trace into this project |
X-Buggregator-Trace-Url | The request you traced, e.g. GET /checkout. Kept with the trace |
X-Buggregator-Trace-Name | A name for the trace, e.g. slow checkout |
Example with all of them:
curl -H 'X-Buggregator-Event: xdebug-trace' \
-H 'X-Buggregator-Project: shop' \
-H 'X-Buggregator-Trace-Url: GET /checkout' \
-H 'X-Buggregator-Trace-Name: slow checkout' \
--data-binary @/tmp/traces/trace.1234567890.xt.gz \
http://127.0.0.1:8000/Errors come back as 400 with a plain-text reason. No file is left behind.
| Reason | Fix |
|---|---|
not an Xdebug trace in the computer-readable format: set xdebug.trace_format=1 | Record again with xdebug.trace_format=1 |
xdebug-trace: upload is larger than 104857600 bytes (xdebug_trace.max_upload) (the number is your limit) | Raise max_upload, or send the .gz file |
In the UI
Traces have their own page: Xdebug traces in the sidebar. Each card shows the duration, peak memory, number of calls and depth. A cut badge means the trace hit the size limit or has no end.
Open a trace to see two tabs:
- Call tree. The calls, one level at a time: click the arrow (▸) next to a call to see the calls it made. A long level ends with Show more calls. The args button, on calls that have them, shows the arguments and return value. Search with Find a function or file, e.g.
PDO->query; click a hit and the tree opens down to it. - Slowest & top. Top self-time lists functions by the time spent in their own code, callees left out. Slowest calls lists single calls by their time with callees.
Limits and storage
xdebug_trace:
max_upload: ${XDEBUG_TRACE_MAX_UPLOAD:100MB}
max_size: ${XDEBUG_TRACE_MAX_SIZE:256MB}
keep: ${XDEBUG_TRACE_KEEP:20}| Setting | Env variable | Default | What it limits |
|---|---|---|---|
max_upload | XDEBUG_TRACE_MAX_UPLOAD | 100MB | The request body, as sent (gzip or not). A larger upload is refused with 400. |
max_size | XDEBUG_TRACE_MAX_SIZE | 256MB | How much of the unpacked trace Buggregator reads. A longer trace is cut there and marked as cut. |
keep | XDEBUG_TRACE_KEEP | 20 | How many traces are kept. A new trace past this number deletes the oldest ones. Pinned traces are never deleted. |
Sizes take a number of bytes or a KB, MB, GB suffix (1 KB = 1024 bytes). A value that is not a size falls back to the default and logs a warning.
Where the files live. Buggregator keeps each file as you sent it, gzip or not:
storage.mode: filesystem: inxdebug-traces/understorage.path(default./storage/xdebug-traces). They stay after a restart, as long as the database does.- Otherwise (the default,
memory): in a temporary folder. The files are gone after a restart. If the database is a file (DATABASE_DSN), Buggregator drops those trace events at start, because their files are gone. The folder isbuggregator-xdebug-traces-<pid>in the system temp folder (TMPDIRon Linux and macOS, else/tmp;%TEMP%on Windows). At start, Buggregator removes such folders left by processes that are no longer running, e.g. after a crash. Several containers share one/tmpvolume? Give each its ownTMPDIR. Each container's server is usually process 1, so they would all use the same folder and remove each other's traces at start. TheTMPDIRfolder must exist: Buggregator also unpacks its VarDumper parser there, and with a missing folder it does not start. With the VarDumper module off, it starts, but the trace module stays off and logs an error. Then Xdebug traces is not in the sidebar. Example:-v /tmp/buggregator-a:/srv/tmp -e TMPDIR=/srv/tmp.
Deleting the trace event deletes its files.
Gzip or plain. Buggregator reads a .xt.gz file from its start on every tree or search request. On a big gzip trace, opening a deep call can take seconds. If that matters, send the trace unpacked (xdebug.use_compression=0), or keep traces small.
Turn the module off:
modules:
xdebug-trace: falseHTTP API
For scripts and agents. Same auth as the rest of /api.
| Route | Returns |
|---|---|
GET /api/xdebug-traces/{uuid}/summary | File info and the summary: calls, user calls, depth, duration, peak memory, top functions by own time, slowest calls, truncated |
GET /api/xdebug-traces/{uuid}/tree?parent=&from=&limit= | One level of calls: the top level without parent, or the calls made by the call at byte offset parent. next is the from of the next page; send it with the same parent |
GET /api/xdebug-traces/{uuid}/search?q=&from=&limit= | Calls that match q (at most 256 bytes). Each result has path: its callers, top-down |
limit is 200 by default, 1000 at most. A bad number gives 400; an unknown trace gives 404.
A summary of a small trace (top and slowest cut to fit here). Times are seconds, memory is bytes:
{
"compressed": true,
"file": "90494b42-e565-4f2e-889f-ea1245ac2916.xt.gz",
"name": "",
"size": 251,
"summary": {
"xdebug_version": "3.4.5",
"file_format": 4,
"start": "2026-11-08 10:00:00.000001",
"end": "2026-11-08 10:00:00.010201",
"end_time": 0.0102,
"end_memory": 419000,
"calls": 3,
"user_calls": 2,
"max_depth": 3,
"duration": 0.0102,
"peak_memory": 452000,
"assignments": 0,
"returns": false,
"truncated": false,
"main": "/app/index.php",
"top": [
{"func": "PDO->query", "user": false, "count": 1, "self": 0.0094, "incl": 0.0094}
],
"slowest": [
{"nr": 0, "func": "{main}", "file": "/app/index.php", "line": 0, "level": 1, "start": 0.0001, "incl": 0.01,
"self": 0.00020000000000000052, "memory": 20000, "offset": 71}
],
"index": [{"nr": 0, "offset": 71, "level": 1}]
},
"url": ""
}top is by time spent in the function itself (self), slowest by time including callees (incl). offset is what you pass as parent to tree.
MCP tools
With MCP on (mcp.enabled: true), an AI assistant can read traces itself:
| Tool | Arguments | Returns |
|---|---|---|
xdebug_traces | project, limit (20, max 100) | Traces, newest first: uuid, project, name, url, script, duration (s), peak memory (bytes), calls, depth, truncated |
xdebug_trace_summary | uuid | Totals, the 20 functions with the most self time, the 20 slowest calls with their offset |
xdebug_trace_calls | uuid, parent, from, query, limit (50, max 200) | No parent: top-level calls. parent=<offset>: its direct callees. A non-null next: call again with from=next and the same parent. query: calls whose function or file contains the text (case ignored), each with path (callers, top-down) |
xdebug_trace_call | uuid, offset | One call (arguments, return value, incl/self time, memory change) and its first 50 direct callees. A non-null from: get the rest with xdebug_trace_calls and the parent and from of this answer |
Arguments and return values are cut to 512 bytes each, at most 20 arguments per call.
A typical path: xdebug_traces → xdebug_trace_summary → xdebug_trace_call on the offset of a slow call → xdebug_trace_calls to go deeper. Example question to the assistant: «Why is the last trace slow?»