Skip to content

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):

ini
xdebug.mode=trace
xdebug.trace_format=1
xdebug.start_with_request=trigger
xdebug.output_dir=/tmp/traces
  • xdebug.mode=trace turns tracing on. You can combine it with other modes, e.g. xdebug.mode=debug,trace.
  • xdebug.trace_format=1 is required. It is the computer-readable format. Buggregator refuses the other formats (0, 2, 3, 4) with 400.
  • xdebug.start_with_request says which requests are traced. trigger traces only a request with the XDEBUG_TRIGGER cookie, query or POST parameter (or env variable for a CLI script). yes traces every request.
  • xdebug.output_dir is where Xdebug writes the files. The default is /tmp.

Optional:

  • xdebug.collect_return=1 adds return values.
  • xdebug.collect_assignments=1 adds variable assignments.
  • xdebug.use_compression (Xdebug 3.1+, on when Xdebug has gzip support) writes .xt.gz files. 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:

bash
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:import

Send the trace to Buggregator ​

Post the file as the request body:

bash
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:

HeaderWhat it does
X-Buggregator-ProjectPuts the trace into this project
X-Buggregator-Trace-UrlThe request you traced, e.g. GET /checkout. Kept with the trace
X-Buggregator-Trace-NameA name for the trace, e.g. slow checkout

Example with all of them:

bash
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.

ReasonFix
not an Xdebug trace in the computer-readable format: set xdebug.trace_format=1Record 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 ​

yaml
xdebug_trace:
  max_upload: ${XDEBUG_TRACE_MAX_UPLOAD:100MB}
  max_size: ${XDEBUG_TRACE_MAX_SIZE:256MB}
  keep: ${XDEBUG_TRACE_KEEP:20}
SettingEnv variableDefaultWhat it limits
max_uploadXDEBUG_TRACE_MAX_UPLOAD100MBThe request body, as sent (gzip or not). A larger upload is refused with 400.
max_sizeXDEBUG_TRACE_MAX_SIZE256MBHow much of the unpacked trace Buggregator reads. A longer trace is cut there and marked as cut.
keepXDEBUG_TRACE_KEEP20How 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: in xdebug-traces/ under storage.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 is buggregator-xdebug-traces-<pid> in the system temp folder (TMPDIR on 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 /tmp volume? Give each its own TMPDIR. Each container's server is usually process 1, so they would all use the same folder and remove each other's traces at start. The TMPDIR folder 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:

yaml
modules:
  xdebug-trace: false

HTTP API ​

For scripts and agents. Same auth as the rest of /api.

RouteReturns
GET /api/xdebug-traces/{uuid}/summaryFile 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:

json
{
  "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:

ToolArgumentsReturns
xdebug_tracesproject, limit (20, max 100)Traces, newest first: uuid, project, name, url, script, duration (s), peak memory (bytes), calls, depth, truncated
xdebug_trace_summaryuuidTotals, the 20 functions with the most self time, the 20 slowest calls with their offset
xdebug_trace_callsuuid, 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_calluuid, offsetOne 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?»