Query Origin Tracer releases
Trace slow and stuck WordPress database queries to their originating requests, visitors, plugins, themes, and PHP call stacks.
by Rocket.net · github.com/andrecmsilva/query-origin-tracer · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/andrecmsilva/query-origin-tracer/releases/download/v2.0.1/query-origin-tracer-2.0.1.zipReadme
Query Origin Tracer
Find 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.
Read the full README on GitHub →