Skip to content

Save Profiler

instance-save-profiler times the classic edit screen and saving a post. It records how long every callback on the save hooks took and which of them ran the database queries, then stores a report you read over WP-CLI.

It is off until you switch it on, and it switches itself off again.

Query Monitor shows what happens while a page renders. A save is a POST that redirects straight away, so there is nothing to look at: the work is done before the next screen loads. The profiler records that request and keeps the result.

It earns its place on sites with large ACF field groups, where a save can run thousands of queries without any single one being slow.

Terminal window
# Profile for an hour, or pick the minutes yourself
ddev wp instance profiler enable
ddev wp instance profiler enable --minutes=15
# Open or save a post in wp-admin, then read the newest report
ddev wp instance profiler show
# Every stored report, and one by number
ddev wp instance profiler list
ddev wp instance profiler show 3
ddev wp instance profiler show --format=json
# Is it on, and how many reports are stored
ddev wp instance profiler status
# Stop early, and delete the reports
ddev wp instance profiler disable
ddev wp instance profiler clear

On a server, drop the ddev prefix. On multisite everything is per site, so add --url to pick one:

Terminal window
wp --url=https://example.com/other-site instance profiler enable

An example report, trimmed, for a page with a large flexible content field:

Save post 496 by editor at 2026-09-15 09:29:39 UTC
Total 1857ms, peak memory 24MB, 241 POST vars (40 KB)
Slowest callbacks. Times are inclusive, so nested hooks overlap:
+--------+-------+-------------------------------------------------------------+
| ms | count | callback |
+--------+-------+-------------------------------------------------------------+
| 1053.4 | 2 | wp_after_insert_post @9 wp_save_post_revision_on_insert |
| 1046.2 | 1 | _wp_put_post_revision @10 acf_revisions::maybe_save_revision |
| 484.1 | 2 | save_post @10 ACF_Form_Post::save_post |
+--------+-------+-------------------------------------------------------------+
6648 queries in 1091ms. By the callback that ran them:
+-------+-------+---------------------------------------------------------+
| ms | count | origin |
+-------+-------+---------------------------------------------------------+
| 765.0 | 5183 | wp_after_insert_post > wp_save_post_revision_on_insert |
| 302.0 | 1249 | save_post > ACF_Form_Post->save_post |
+-------+-------+---------------------------------------------------------+
  • Slowest callbacks are timed individually. A callback that fires another hook includes that hook’s time, so the times do not add up to the total.
  • By origin groups every query under the hook callback that ran it. This is usually where the answer is.
  • Most repeated queries shows the shape of each query with its values replaced. A query run once per field is the thing to look for.

In the report above, saving a revision took over half the request: ACF copies every field value into each revision, and core reloads the revision’s meta before every copied field. On a site like that, turning revisions off (add_filter('wp_revisions_to_keep', '__return_zero')) removes that work from every save.

Only wp-admin/post.php: opening a post for editing and saving it. Every other request returns before the plugin reads anything from the database, so it costs nothing to leave installed.

The block editor saves over the REST API and is not covered. Lumbermill disables the block editor, so posts save through post.php.

While a request is profiled, SAVEQUERIES is turned on for it. If a config file already sets SAVEQUERIES to false, the report has callback timings but no query breakdown.

Reports live in the instance_save_profiler_reports option, newest first, and are not autoloaded. Once the limit is reached the oldest report is dropped.

Nothing is written to a log file: web/app is served publicly, and a report names plugins, callbacks and file paths. String and number literals are stripped from queries before they are stored, so a report never holds the content that was saved.

When instance-audit-log is active, enable and disable are recorded there.

SettingDefaultMeaning
INSTANCE_SAVE_PROFILER_REPORTS10Reports kept per site
INSTANCE_SAVE_PROFILER_MINUTES60How long enable lasts without --minutes

Override them in config/application.php:

config/application.php
Config::define('INSTANCE_SAVE_PROFILER_REPORTS', 20);
FilterUse
instance_save_profiler_hooksHooks whose callbacks are timed one by one

save_post_{post_type} and edit_post_{post_type} are added for the post being profiled.