wp dbtk api call <method> <route> --profile calls any registered REST endpoint through WordPress’s internal REST server and returns structured JSON with a full performance breakdown: execution time, memory usage, database query count, per-component query attribution, duplicate detection, and the five slowest queries with their SQL. This makes terminal-based profiling the right approach for headless WordPress setups, CI pipelines, and AI-assisted development sessions where browser-based profilers are not an option.
To use these commands from an AI coding assistant, install the WPDT agent skill:
bash
npx skills add WP-Debug-Toolkit/wpdt-cliSee How to Use WP Debug Toolkit with AI Coding Assistants and CLI Environment Setup for full setup details.
Run the command with --profile appended:
bash
wp dbtk api call GET /wc/v3/products --params='{"per_page":3}' --profileWPDT dispatches the request through WordPress’s REST server internally using rest_do_request(). There is no HTTP connection, DNS lookup, or network overhead. The timing numbers reflect only PHP execution and database query time.
The response contains four top-level keys. The profile block is appended as a sibling to status, data, and headers:
json
{
"status": 200, // HTTP status code
"data": [ ... ], // REST response body
"headers": { // REST response headers
"X-WP-Total": 20,
"X-WP-TotalPages": 7
},
"profile": { // Profiling data; not part of the REST response
"execution_time_ms": 19.63, // Total PHP execution time in milliseconds
"memory": {
"delta_mb": 0.18, // Memory consumed by this request
"peak_mb": 117.79 // Peak memory usage during the request
},
"queries": {
"timings_available": true, // false if SAVEQUERIES could not be enabled
"total": 22, // Total queries triggered by the endpoint
"slow": 0, // Queries exceeding the 50ms threshold
"duplicates": 12, // Redundant executions after normalization (N-1 per repeated pattern)
"by_type": {
"SELECT": 22 // Counts by SQL statement type
},
"total_time_ms": 18.67, // Total time spent in the database
"by_component": {
"wordpress-core": 19, // Queries from WordPress core
"woocommerce": 3 // Queries from WooCommerce
},
"slowest": [] // Top 5 slowest queries; empty when slow === 0
},
"php_errors": [] // PHP errors and warnings (full mode only)
}
}When the endpoint returns an error, the response shape changes. There is no data or headers key. WPDT returns status and an error object instead:
json
{
"status": 401,
"error": {
"code": "rest_forbidden",
"message": "Sorry, you are not allowed to do that."
}
}If you passed --profile, the profile block is still included alongside status and error.
✅ What You Should See: A JSON object with "status": 200 for a successful call and a profile block containing timing, memory, and query data. A "status": 401 or "status": 403 response means the request needs a different user. Add --user=<id> to authenticate as a specific WordPress user.
| Field | What It Tells You |
profile.execution_time_ms | Total time from dispatch to response in milliseconds, including PHP execution and all database queries. Use this as your primary before/after benchmark when measuring the impact of a code change. |
profile.memory.delta_mb | Memory consumed by the endpoint beyond what was already allocated when the request started. A high delta on a simple endpoint suggests unnecessary object loading or unneeded data retrieval. |
profile.memory.peak_mb | Peak memory usage during the request. Compare this against your server’s memory_limit to gauge available headroom for concurrent requests. |
profile.queries.timings_available | true when WPDT can capture per-query timing data (either SAVEQUERIES was already active or WPDT enabled it for this request). When false, the slow and total_time_ms fields are null, slowest is an empty array, and a capture_warning message explains why. |
profile.queries.total | Total database queries the endpoint triggered. A typical WordPress page load runs 20 to 40 queries; a well-optimized REST endpoint returning a list should stay in that range or below. 100-plus queries on a simple endpoint is a strong signal of an N+1 pattern. |
profile.queries.slow | Queries that exceeded the 50ms slow threshold. Even a single slow query can account for the majority of execution_time_ms. |
profile.queries.duplicates | The number of redundant query executions. For each distinct query pattern that ran N times after normalization, this adds N−1. A high count usually indicates an N+1 pattern, where related data is fetched one item at a time inside a loop instead of batch-loaded before the loop starts. |
profile.queries.by_type | Query count broken down by SQL statement type (SELECT, INSERT, UPDATE, etc.). A read endpoint generating INSERT or UPDATE statements signals unintended side effects such as transient refreshes or audit logging. |
profile.queries.total_time_ms | Total time the endpoint spent executing database queries. Compare this against execution_time_ms to determine whether the bottleneck is in the database or in PHP logic between queries. |
profile.queries.by_component | Query count per plugin, theme, or WordPress core. WPDT identifies each component by inspecting the call stack and matching the originating file path against plugin, theme, and core path patterns. No configuration is required. |
profile.queries.slowest | The five slowest queries, each with its sql, time_ms, and component. Only populated when at least one query exceeds the 50ms threshold. |
profile.php_errors | PHP errors, warnings, notices, and deprecations that fired during the request. Only captured when using --profile or --profile=full. |
Profile the endpoint and examine by_component:
bash
wp dbtk api call GET /wc/v3/products --params='{"per_page":5}' --profile
For example, if the output includes:
json
"by_component": {
"wordpress-core": 19,
"woocommerce": 3,
"my-discount-plugin": 8
}It means that your plugin adds 8 queries to a request that would otherwise run 22 queries. Whether that is acceptable depends on the endpoint’s complexity, but now you have a number rather than a guess.
If your plugin does not appear in by_component at all, it is not triggering any database queries on that endpoint.
Save the profile before making changes:
bash
wp dbtk api call GET /wc/v3/products --params='{"per_page":5}' --profile > /tmp/before.jsonMake your change, then profile again:
bash
wp dbtk api call GET /wc/v3/products --params='{"per_page":5}' --profile > /tmp/after.jsonIf you have jq installed, compare the key metrics directly:
bash
# Before
cat /tmp/before.json | jq '.profile.queries | {total, slow, duplicates, total_time_ms}'
# After
cat /tmp/after.json | jq '.profile.queries | {total, slow, duplicates, total_time_ms}'A meaningful improvement: total down, duplicates down, total_time_ms down. If the numbers are unchanged, the bottleneck is elsewhere in the request lifecycle.
The queries.duplicates field counts redundant query executions, not distinct repeated patterns. If the same normalized query ran 13 times, it contributes 12 to this count. WPDT normalizes SQL by replacing single-quoted and double-quoted string literals, standalone numeric values, and IN (...) clause contents before comparing.
This means SELECT * FROM wp_postmeta WHERE post_id = 42 and SELECT * FROM wp_postmeta WHERE post_id = 87 register as the same query pattern.
A high duplicate count almost always indicates an N+1 pattern. Common examples in WordPress plugins:
update_meta_cache()update_object_term_cache()Check queries.slowest as well. If the same query pattern appears multiple times among the five slowest entries, that pattern is your primary target.
For the full mechanism behind slow query detection and duplicate identification, see Slow Query, N+1, and Duplicate Detection.
Three modes control how much data --profile collects:
bash
# Full mode (default): execution time, memory, complete query analysis, PHP error capture
wp dbtk api call GET /wp/v2/posts --profile
# Queries mode: execution time, memory, complete query analysis (no PHP error capture)
wp dbtk api call GET /wp/v2/posts --profile=queries
# Summary mode: execution time, memory, and total query count only
wp dbtk api call GET /wp/v2/posts --profile=summary--profile without a mode value behaves identically to --profile=full. Use summary for quick sanity checks between iterations. Use --profile=queries when you need the complete query breakdown without the overhead of PHP error capture. Use full when you need both.
For a conceptual comparison of what each mode captures and how WPDT hooks into query logging, see Profiling Endpoint Performance.
If you are not sure which endpoints to profile, use the discovery commands:
bash
# Scan and store all registered REST routes on this site
wp dbtk api discover
# Search routes by keyword
wp dbtk api search "order"
# Inspect a specific route: methods, parameters, and auth requirements
wp dbtk api show /wc/v3/ordersThen profile whatever you find:
bash
wp dbtk api call GET /wc/v3/orders --params='{"status":"processing"}' --profileThe profiler works on any REST endpoint registered on the site, including WordPress core, WooCommerce, Yoast, Elementor, and any custom plugin.
WPDT uses the current WP-CLI user identity for the dispatched request. Pass --user=<id> to authenticate as a specific WordPress user:
bash
wp dbtk api call GET /wc/v3/products --profile --user=1A "status": 401 means no authenticated user was attached to the request. A "status": 403 means a user was authenticated but lacks the required capabilities. Either way, pass --user=<admin-user-id> to authenticate as a user with full access.
wp dbtk api call --profile make a real HTTP request to my site?No. WPDT dispatches the request through WordPress’s REST server using rest_do_request(), with no HTTP connection, DNS resolution, or network overhead. The execution_time_ms value in the profile reflects only PHP execution time and database query time for the endpoint itself, giving you timing data that isolates application performance from network conditions.
Context determines the answer, but a few benchmarks help. A typical WordPress page load runs 20 to 40 queries. A simple REST endpoint returning a single object should stay well under 20. An endpoint returning a list of 10 items that generates 80 to 100 queries is almost certainly hitting an N+1 pattern. Use by_component to find where the queries originate and duplicates to confirm whether the same pattern is repeating.
Profiling Endpoint Performance – Learn how each profile mode scopes its data capture and why query profiling works on any site regardless of SAVEQUERIES state.
Slow Query, N+1, and Duplicate Detection – Dig into the detection logic behind the slow, duplicates, and slowest fields so you know exactly what you’re looking at when the numbers are high.
How to Use WP Debug Toolkit with AI Coding Assistants – Get your AI coding assistant running wp dbtk api call --profile directly from its terminal without manual configuration.