The --profile flag on wp dbtk api call gives you timing, memory, query counts, component attribution, and PHP error capture — all from a single CLI invocation, without touching a browser or adding instrumentation code.
| Category | Fields |
|---|---|
| Timing | execution_time_ms — total wall-clock time for the endpoint |
| Memory | memory.delta_mb — memory consumed by the request; memory.peak_mb — PHP peak usage |
| Queries | queries.total, .slow, .duplicates, .by_type, .total_time_ms, .by_component, .slowest |
| PHP errors | php_errors — array of PHP warnings, notices, and errors captured during execution |
All measurements are scoped to the internal dispatch of that single endpoint call. They do not include WP-CLI bootstrap overhead.
Full profile output is returned as a profile key alongside the normal status and data fields.
Example output (when queries are captured)
{
"status": 200,
"data": { "...endpoint response..." },
"profile": {
"execution_time_ms": 142.5,
"memory": {
"delta_mb": 2.4,
"peak_mb": 14.2
},
"queries": {
"total": 23,
"slow": 2,
"duplicates": 3,
"by_type": { "SELECT": 20, "INSERT": 2, "UPDATE": 1 },
"total_time_ms": 89.3,
"by_component": {
"woocommerce": 18,
"wordpress-core": 5
},
"slowest": [
{
"sql": "SELECT * FROM wp_postmeta WHERE post_id IN (...)",
"time_ms": 34.2,
"component": "woocommerce"
}
]
},
"php_errors": []
}
}Field-by-field reference
| Field | Type | Description |
|---|---|---|
execution_time_ms |
float | Total endpoint execution time in milliseconds |
memory.delta_mb |
float | Memory used by this request (peak minus baseline), in MB |
memory.peak_mb |
float | PHP peak memory usage at the end of the request, in MB |
queries.total |
int | Total number of database queries executed |
queries.slow |
int | Queries exceeding the slow query threshold (default: 50ms) |
queries.duplicates |
int | Number of queries that were run more than once with identical SQL |
queries.by_type |
object | Query counts by verb: SELECT, INSERT, UPDATE, DELETE |
queries.total_time_ms |
float | Sum of all query execution times in milliseconds |
queries.by_component |
object | Query counts keyed by plugin/theme slug that triggered them |
queries.slowest |
array | Top slowest queries, each with sql, time_ms, and component |
php_errors |
array | PHP errors, warnings, or notices caught during execution |
Use the --profile flag with an optional mode argument:
--profile # Full profile (same as --profile=full)
--profile=full # All data: timing, memory, queries, PHP errors
--profile=queries # Query data only
--profile=summary # Timing + counts onlyMode comparison
| Field | full |
queries |
summary |
|---|---|---|---|
execution_time_ms |
yes | — | yes |
memory |
yes | — | yes |
queries.total |
yes | yes | yes |
queries.slow |
yes | yes | yes |
queries.duplicates |
yes | yes | — |
queries.by_type |
yes | yes | — |
queries.by_component |
yes | yes | — |
queries.slowest |
yes | yes | — |
php_errors |
yes | — | — |
Use summary for a quick sanity check. Use queries when you care about the database only. Use full (the default) when investigating.
Profile an endpoint that your plugin touches, then check by_component.
# Profile a WooCommerce product listing endpoint
wp dbtk api call GET /wc/v3/products --params='{"per_page":10}' --profileIn the output, look at profile.queries.by_component:
"by_component": {
"woocommerce": 18,
"my-plugin": 12,
"wordpress-core": 5
}If your plugin slug appears with a high query count, those queries are being triggered by your code. Cross-reference with queries.slowest to find which specific queries are the worst offenders:
"slowest": [
{
"sql": "SELECT * FROM wp_postmeta WHERE post_id IN (1,2,3,...)",
"time_ms": 34.2,
"component": "my-plugin"
}
]This tells you both what the query is and which plugin triggered it. Fix the query (add an index, batch the lookup, cache the result) and re-profile to confirm improvement.
Capture a baseline, make your change, then compare.
Step 1: Capture baseline
wp dbtk api call GET /wp/v2/posts --params='{"per_page":20}' --profile > /tmp/before.jsonStep 2: Make your change
Deploy your code change (update a plugin, add an index, tweak a query, etc.).
Step 3: Capture after
wp dbtk api call GET /wp/v2/posts --params='{"per_page":20}' --profile > /tmp/after.jsonStep 4: Compare
# Quick comparison of key metrics
jq '.profile.execution_time_ms, .profile.queries.total, .profile.queries.slow' /tmp/before.json /tmp/after.jsonOr diff the full profiles:
jq '.profile' /tmp/before.json > /tmp/before-profile.json
jq '.profile' /tmp/after.json > /tmp/after-profile.json
diff /tmp/before-profile.json /tmp/after-profile.jsonLook for reductions in execution_time_ms, queries.total, and queries.slow. A meaningful improvement shows up as lower counts across all three.
The queries.duplicates field counts how many queries were executed more than once with identical SQL. This is the N+1 indicator.
wp dbtk api call GET /wc/v3/orders --params='{"per_page":20}' --profile=queriesIf duplicates is non-zero:
"queries": {
"total": 62,
"slow": 0,
"duplicates": 18,
...
}18 duplicate queries on a 20-item list strongly suggests an N+1 problem: code is running the same query once per item in a loop rather than batching.
Switch to --profile=full and inspect queries.slowest — duplicated queries often appear in the slowest list when they accumulate across many items. Look for queries with post_id = X where X varies, or meta_key = 'some_key' running per row.
Fixing N+1 problems
The typical fix is replacing per-item queries with a single batch query using IN (...) or caching results in a static variable. After the fix, duplicates should drop to 0 or near 0.
queries.by_component maps each plugin/theme slug to the number of queries it triggered during the endpoint call. Component attribution works by inspecting the PHP call stack of each query and identifying the first frame that belongs to a known plugin or theme directory.
Reading the by_component output
"by_component": {
"woocommerce": 18,
"wordpress-core": 5,
"my-custom-plugin": 3,
"some-theme": 1
}This tells you:
- WooCommerce ran 18 queries (expected — it's the endpoint's plugin)
- WordPress core ran 5 queries (normal: options, user, nonce checks)
- Your custom plugin ran 3 queries (worth investigating if unexpected)
- The active theme ran 1 query (check if theme code is hooking into REST responses)
Tracking down unexpected contributors
If a plugin you didn't expect shows up, profile the endpoint without that plugin active (if safe to do so) and compare. If its query count drops to 0, that plugin is the source.
You can also use queries.slowest filtered by component to find which specific queries belong to that plugin.
Query capture (everything under queries.* except queries.total if it falls back to a counter) requires SAVEQUERIES to be true in WordPress.
How to enable it
wp dbtk query-log onThis enables both SAVEQUERIES and enhanced query logging.
Known caveat
There is a known issue where SAVEQUERIES may already be defined as false during WP-CLI bootstrap, before the plugin can override it. When this happens, query data will not be captured for that invocation and query fields will be absent or show zeros — even though SAVEQUERIES is technically enabled in the database setting.
Workaround
If you see missing query data despite having query logging enabled, try:
- Confirm
wp dbtk debug statusshowsSAVEQUERIES: ON - Re-run the profile command — the issue is intermittent
- If it persists, check for other plugins or must-use plugins that define
SAVEQUERIESearly in the bootstrap sequence
Timing and memory data (execution_time_ms, memory) are always captured regardless of SAVEQUERIES state.