Query Origin Tracer WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA
Trace slow and stuck WordPress database queries to their originating requests, visitors, plugins, themes, and PHP call stacks.
Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/andrecmsilva/query-origin-tracer/releases/download/v2.0.1/query-origin-tracer-2.0.1.zipFind the WordPress request, visitor, plugin, theme, or PHP call stack behind a database query.
Query Origin Tracer is a production diagnostics plugin for investigating slow, repeated, and stuck WordPress database queries. It records structured JSONL evidence and includes WP-CLI commands for starting targeted captures, following results live, and cleaning up afterward.
[!WARNING] This is a temporary diagnostic tool, not an always-on profiler. Captured SQL, request data, and headers may contain sensitive information. Use it for focused investigations and disable it when the capture is complete.
Why use it?
A slow query log can tell you what ran. Query Origin Tracer helps answer the harder questions:
- Which request caused it?
- Which plugin, MU-plugin, theme, or application code initiated it?
- What PHP stack led to the query?
- Which visitor or upstream request was involved?
- Is a query currently stuck inside a PHP worker?
- Are recurring queries the same shape with different values?
What it captures
- Raw SQL and a normalized query fingerprint
- Execution time for completed queries
- PHP call stack with file and line numbers
- Detected plugin, MU-plugin, theme, or application origin
- Request URI, method, host, referer, and user agent
REMOTE_ADDR,CF-Connecting-IP,True-Client-IP,X-Forwarded-For, andCF-Ray- REST, AJAX, cron, WP-CLI, admin, and XML-RPC context
- Authenticated WordPress user ID when already available
- PID, request ID, site/blog ID, current memory, and peak memory
- Optional per-PID records for queries that have started but not returned
How v2.0 works
Query Origin Tracer installs as a normal plugin in wp-content/plugins/query-origin-tracer/. When activated, it creates a small managed bootstrap at:
wp-content/mu-plugins/query-origin-tracer-loader.php
The bootstrap loads the tracer early enough to observe queries initiated before ordinary plugins load, while the plugin itself remains manageable through the standard WordPress lifecycle.
- Activation creates the early loader and activation marker.
- Deactivation removes the loader and marker but preserves captured evidence.
- Deletion removes the loader, marker, configuration, logs, in-flight records, stored options, and any legacy MU-only installation.
- Capture is opt-in: activating the plugin alone does not begin tracing queries.
Requirements
- WordPress 5.8 or newer
- PHP 7.4 or newer
- WP-CLI for capture management and the live reader
Installation
WordPress Admin
- Download the latest release ZIP.
- Open Plugins → Add New → Upload Plugin.
- Upload, install, and activate Query Origin Tracer.
WP-CLI
wp plugin install /path/to/query-origin-tracer.zip --activate
Manual
cp -a query-origin-tracer wp-content/plugins/
wp plugin activate query-origin-tracer
The default state directory for a typical /home/USER/public_html installation is outside the document root:
/home/USER/.query-origin-tracer
To use another location, define it in wp-config.php before WordPress loads:
define( 'QOT_STATE_DIR', '/secure/path/.query-origin-tracer' );
Quick start
Capture queries taking at least 250 ms:
wp query-origin enable --mode=slow --min-seconds=0.25
Capture starts on subsequent WordPress requests. Open the site from another terminal or browser, making sure the request reaches WordPress rather than being served entirely from a page cache or CDN.
Follow the latest records live:
wp query-origin list
list is intentionally blocking: it continues waiting for new matching queries until you press Ctrl+C. The default slow mode may remain empty on a healthy site because it only records individual database queries taking at least 250 ms.
For a first-run verification, temporarily capture every query, generate one uncached request, then print the results without following:
wp query-origin enable --mode=all --max-per-request=25
curl -sS "https://example.com/?qot-test=$(date +%s)" >/dev/null
wp query-origin list --last=25 --no-follow
wp query-origin disable
Run the curl request from another terminal if the first terminal is already following the log.
Check the current configuration:
wp query-origin status
When the investigation is complete:
wp query-origin disable
Disabling capture preserves the evidence. Remove it explicitly with:
wp query-origin clear
Capture recipes
Find actively stuck queries
In-flight mode maintains one current-query file per PHP PID. If a query never returns, its record remains available for inspection.
wp query-origin enable --mode=slow --min-seconds=0.25 --inflight=1
wp query-origin inflight
Or inspect recent records directly:
find ~/.query-origin-tracer/inflight -type f -name '*.json' -mmin -10 -print -exec jq . {} \;
Capture every completed query
Use this only for short, targeted windows:
wp query-origin enable --mode=all --max-per-request=100
Filter by SQL
Regular expressions must include delimiters:
wp query-origin enable \
--mode=all \
--sql-include='~wp_actionscheduler_|SQL_CALC_FOUND_ROWS|wp_postmeta~i' \
--inflight=1
Filter by request
wp query-origin enable \
--mode=all \
--request-include='~wp-json|admin-ajax\.php|wp-cron\.php~i'
Filter by visitor IP
IP matching uses exact values:
wp query-origin enable --mode=all --ip='203.0.113.10,2001:db8::10'
Reading results
Live WP-CLI reader
wp query-origin list prints the latest 10 records and continues following new queries until Ctrl+C. Each result includes the host, request URI and query string, remote address, user agent, UTC start time, SQL, detected origin, duration, and current/peak PHP memory.
# Start with the latest 25 records and continue following.
wp query-origin list --last=25
# Follow only records written after the command starts.
wp query-origin list --last=0
# Print the latest 50 records and exit.
wp query-origin list --last=50 --no-follow
# Poll the log every 100 milliseconds.
wp query-origin list --poll=0.10
The reader handles ordinary appends, log truncation after clear, and log-file recreation.
Raw JSONL
tail -F ~/.query-origin-tracer/events.jsonl | jq .
Group recurring query shapes:
jq -r '.fingerprint' ~/.query-origin-tracer/events.jsonl \
| sort | uniq -c | sort -nr | head -30
Group queries by detected component:
jq -r '[.origin.type, .origin.component] | @tsv' ~/.query-origin-tracer/events.jsonl \
| sort | uniq -c | sort -nr | head -30
Show stack traces for queries over one second:
jq -r '
select(.duration_seconds >= 1)
| "\n=== \(.duration_ms) ms | \(.request_uri) ===\n"
+ (.stack[] | "\(.file):\(.line) \(.call)")
' ~/.query-origin-tracer/events.jsonl
Command reference
| Command | Purpose |
|---|---|
wp query-origin enable |
Start or update a capture |
wp query-origin status |
Show configuration, log location, and in-flight count |
wp query-origin list |
Read and optionally follow completed-query records |
wp query-origin inflight |
Display queries currently associated with PHP PIDs |
wp query-origin disable |
Stop tracing while retaining evidence |
wp query-origin clear |
Delete completed-query and in-flight records |
Operational and privacy notes
SAVEQUERIEStimes every$wpdbquery and collects caller information. Expect additional CPU and memory overhead.- In-flight mode writes and removes a small file for each eligible query. Enable it only while investigating active hangs.
- SQL, URIs, headers, and user metadata can contain personal data, credentials, or tokens. Protect the state directory and sanitize evidence before sharing it.
- State directories and files are created with restrictive permissions and should remain outside the document root.
CF-Connecting-IPis trustworthy only when the origin accepts traffic exclusively from Cloudflare or otherwise prevents clients from spoofing it.
Limitations
- With the managed MU loader, tracing begins after MU-plugins load. If the server prevents the loader from being created, the tracer still runs as an ordinary plugin but begins later in bootstrap.
- It cannot observe bootstrap queries that ran before the tracer loaded.
- It cannot observe direct
mysqlior PDO connections created outside the active WordPress$wpdbimplementation. - It cannot observe queries avoided entirely by object caching.
- A custom
db.phpdrop-in must implement WordPress'sSAVEQUERIESandlog_query_custom_databehavior for completed-query timing. In-flight capture may still help when the standardqueryfilter is implemented.
Upgrade from v1.x
Version 2.0 replaces the legacy MU-only package with a standard WordPress plugin. Activation and uninstall routines detect and clean up the earlier package automatically. Install and activate v2.0 through the normal plugin workflow; it will manage the early bootstrap from there.
Changelog
2.0.1
- Clarified that capture begins on subsequent WordPress requests.
- Added actionable first-run guidance when no completed query has matched.
- Explained the live reader's blocking behavior and all-query verification workflow.
2.0.0
- Converted the legacy MU-only package into a standard WordPress plugin.
- Added an activation-managed early MU bootstrap.
- Added complete deactivation, uninstall, and v1 migration cleanup.
Verify or remove the installation
wp plugin status query-origin-tracer
wp query-origin status
ls -l wp-content/mu-plugins/query-origin-tracer-loader.php
To remove the tracer and all plugin-created diagnostic data:
wp plugin deactivate query-origin-tracer
wp plugin delete query-origin-tracer
License
Query Origin Tracer is licensed under the GNU General Public License v2.0 or later.
Releases
3 releases. Each count is every asset in that release; expand a row for the breakdown.