Daniel really pulled out all the stops when he created the wp profile command – kind of like a New Relic alternative (see this great Kinsta post about using New Relic) it can help pinpoint which components are slowing your WordPress site down. Originally available from runcommand as a premium package, it is now free on github.
If you are looking for a host that allows you to install wp profile check out Kinsta and Cloudways.
I use wp profile regularly to diagnose performance issues for my clients on Codeable. rtcamp used it to identify a bad loop as well, all WordPress developers should be using this! After finishing this guide you will be one step closer to fixing your slow WordPress site.
Using WP-CLI wp profile to Diagnose WordPress Performance Issues
This will show you how to install wp profile and use it effectively to identify performance bottlenecks on your site. I recommend testing wp profile on staging servers so there are no other variables affecting load times.
Install wp-profile package
Fire off this command to install wp profile.
wp package install wp-cli/profile-commandYou should see that the process was successful and the wp profile command is now available
Installing package wp-cli/profile-command (dev-master)
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
Resolving dependencies through SAT
Dependency resolution completed in 0.220 seconds
Analyzed 4579 packages to resolve dependencies
Analyzed 190261 rules to resolve dependencies
Package operations: 0 installs, 0 updates, 0 removals
Generating autoload files
---
Success: Package installed.If you see this error then updating php.ini for cli to have max_memory or memory_limit = 256M or higher (up to 2G) should fix it. See this post for more information.
Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 4096 bytes) in phar:///usr/local/bin/wp/vendor/composer/composer/src/Composer/DependencyResolver/RuleWatchGraph.php on line 52You can update the php-cli’s php.ini to use 1G for memory_limit temporarily with this command so you don’t have to edit the php.ini directly
php -d memory_limit=1G $(which wp) package install wp-cli/profile-command --allow-rootIf you get the error that /usr/bin/wp wasn’t found you can find the path to WP-CLI with the which command
which wpIf wp isn’t part of your PATH environment variables you will have to specify the path to WP-CLI’s phar here /usr/bin/local/wp
php -d memory_limit=1G /usr/bin/local/wp package install wp-cli/profile-command --allow-rootIf you see this error about composer.json reverted, updating the php.ini max_memory should help
Updating dependencies
Reverted composer.json.If you see this error ‘Killed‘ when trying to install wp profile it can be fixed by using a swap file usually if the memory increase in php.ini doesn’t help.
Installing package wp-cli/profile-command (dev-master)
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
KilledThis creates a 1 GB swap file. You will need root access to create a swap file.
swapoff -a
dd if=/dev/zero of=/swapfile bs=1M count=1024You will see this output
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.16717 s, 495 MB/sChange the permissions of the swap file and turn it into swap
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfileThen re-run the wp profile installation command
wp package install wp-cli/profile-commandNow you should see the successful output
Updating /root/.wp-cli/packages/composer.json to require the package...
Using Composer to install the package...
---
Loading composer repositories with package information
Updating dependencies
Resolving dependencies through SAT
Dependency resolution completed in 0.120 seconds
Analyzed 3696 packages to resolve dependencies
Analyzed 101022 rules to resolve dependencies
Package operations: 1 install, 0 updates, 0 removals
Installs: wp-cli/profile-command:dev-master ef44df5
- Installing wp-cli/profile-command (dev-master ef44df5)
Writing lock file
Generating autoload files
---
Success: Package installed.Upgrade wp profile
You can upgrade to the latest profile package version with this command
wp package update --allow-rootUsing wp profile Command
Enter WP directory (or use --path at the end of your WP-CLI command)
wp profile can show variable results, it’s best to run it several times when benchmarking!
wp profile stage command
This command shows the stages of loading WordPress.
wp profile stage --allow-rootYou will get this very thorough table. Your main focus should generally be the time column.
In general you want your cache_ratio to be high like greater than 70%
Generally you want cache_hits to be greater than cache_misses
Your query_time should be low as well
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| stage | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | hook_time | hook_count | request_time | request_count |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| bootstrap | 0.8193s | 0.004s | 28 | 92.7% | 559 | 44 | 0.1279s | 2972 | 0s | 0 |
| main_query | 0.0136s | 0.0039s | 7 | 87.25% | 130 | 19 | 0.0042s | 219 | 0s | 0 |
| template | 0.2741s | 0.011s | 30 | 96.94% | 2442 | 77 | 0.2334s | 7030 | 0s | 0 |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| total (3) | 1.107s | 0.0189s | 65 | 92.3% | 3131 | 140 | 0.3654s | 10221 | 0s | 0 |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+Can drill down into each stage, here we drill down into the bootstrap stage
wp profile stage bootstrap --allow-rootYou will then see the hooks used by that stage
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook | callback_count | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before | | 0.3076s | 0.0015s | 4 | 76.47% | 39 | 12 | 0s | 0 |
| muplugins_loaded | 2 | 0.0002s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| plugins_loaded:before | | 0.1897s | 0.003s | 12 | 93.28% | 222 | 16 | 0s | 0 |
| plugins_loaded | 33 | 0.0354s | 0s | 0 | 100% | 44 | 0 | 0s | 0 |
| setup_theme:before | | 0.0003s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| setup_theme | 1 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| after_setup_theme:before | | 0.0149s | 0s | 0 | 100% | 40 | 0 | 0s | 0 |
| after_setup_theme | 20 | 0.0068s | 0s | 0 | 100% | 20 | 0 | 0s | 0 |
| init:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| init | 55 | 0.0261s | 0.0009s | 12 | 92.08% | 186 | 16 | 0s | 0 |
| wp_loaded:before | | 0.0001s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| wp_loaded | 8 | 0.0001s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_loaded:after | | 0.0265s | 0s | 0 | | 0 | 0 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (13) | 119 | 0.6078s | 0.0054s | 28 | 95.76% | 559 | 44 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
I usually prefer to use the –-all flag for the stage command
wp profile stage --all --orderby=time --allow-rootThis will show all of the hooks used by each stage
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook | callback_count | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before | | 0.178s | 0.0009s | 1 | 25% | 1 | 3 | 0s | 0 |
| muplugins_loaded | 2 | 0.0003s | 0s | 0 | 50% | 1 | 1 | 0s | 0 |
| plugins_loaded:before | | 0.2855s | 0.0041s | 19 | 83.95% | 136 | 26 | 0s | 0 |
| plugins_loaded | 34 | 0.2442s | 0.0009s | 3 | 98.31% | 116 | 2 | 0s | 0 |
| setup_theme:before | | 0.0005s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| setup_theme | 1 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| after_setup_theme:before | | 0.2653s | 0.0011s | 3 | 99.68% | 615 | 2 | 0s | 0 |
| after_setup_theme | 17 | 0.0241s | 0.0002s | 1 | 98.8% | 82 | 1 | 0s | 0 |
| init:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| init | 82 | 0.261s | 0.0016s | 9 | 98.74% | 703 | 9 | 0s | 0 |
| wp_loaded:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_loaded | 7 | 0.0012s | 0s | 0 | 100% | 6 | 0 | 0s | 0 |
| parse_request:before | | 0.0192s | 0s | 0 | 100% | 14 | 0 | 0s | 0 |
| parse_request | 2 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| send_headers:before | | 0.0001s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| send_headers | 0 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| pre_get_posts:before | | 0.0003s | 0s | 0 | 100% | 10 | 0 | 0s | 0 |
| pre_get_posts | 8 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| the_posts:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| the_posts | 2 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp | 10 | 0.068s | 0.0148s | 30 | 97.3% | 756 | 21 | 0s | 0 |
| template_redirect:before | | 0.0005s | 0s | 0 | | 0 | 0 | 0s | 0 |
| template_redirect | 20 | 0.0098s | 0.0005s | 1 | 99.06% | 105 | 1 | 0s | 0 |
| template_include:before | | 0.0004s | 0s | 0 | 100% | 9 | 0 | 0s | 0 |
| template_include | 1 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_head:before | | 0.0028s | 0.0003s | 2 | 85.71% | 18 | 3 | 0s | 0 |
| wp_head | 42 | 0.0887s | 0.004s | 9 | 97.85% | 682 | 15 | 0s | 0 |
| loop_start:before | | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wp_footer | 14 | 0.0068s | 0s | 0 | 100% | 61 | 0 | 0s | 0 |
| wp_footer:after | | 0.0002s | 0s | 0 | | 0 | 0 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (31) | 242 | 1.4571s | 0.0284s | 78 | 90.8% | 3323 | 84 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
You can also use the --spotlight flag to filter out zero-like values for easier reading
wp profile stage --all --spotlight --orderby=time --allow-rootMuch cleaner now 🙂
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook | callback_count | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before | | 0.2969s | 0.0016s | 1 | 25% | 1 | 3 | 0s | 0 |
| muplugins_loaded | 2 | 0.0002s | 0s | 0 | 50% | 1 | 1 | 0s | 0 |
| plugins_loaded:before | | 0.2374s | 0.0021s | 19 | 83.95% | 136 | 26 | 0s | 0 |
| plugins_loaded | 34 | 0.2094s | 0.0004s | 3 | 98.31% | 116 | 2 | 0s | 0 |
| after_setup_theme:before | | 0.1991s | 0.0006s | 3 | 99.68% | 615 | 2 | 0s | 0 |
| after_setup_theme | 17 | 0.0299s | 0.0002s | 1 | 98.8% | 82 | 1 | 0s | 0 |
| init | 82 | 0.3435s | 0.001s | 9 | 98.74% | 703 | 9 | 0s | 0 |
| parse_request:before | | 0.0274s | 0s | 0 | 100% | 14 | 0 | 0s | 0 |
| wp | 10 | 0.0747s | 0.0078s | 30 | 97.3% | 756 | 21 | 0s | 0 |
| template_redirect | 20 | 0.0102s | 0.0002s | 1 | 99.06% | 105 | 1 | 0s | 0 |
| wp_head:before | | 0.0039s | 0.0004s | 2 | 85.71% | 18 | 3 | 0s | 0 |
| wp_head | 42 | 0.0884s | 0.0021s | 9 | 97.85% | 682 | 15 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (12) | 207 | 1.5211s | 0.0163s | 78 | 86.2% | 3229 | 84 | 0s | 0 |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+You can use the hook command to drill down into specific hooks.
wp profile hook Command
Here we dig into the wp hook
wp profile hook wp --allow-rootHere are all the functions being called in the wp hook for this particular site running Avada
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| callback | location | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| a3_lazy_load_instance() | a3-lazy-load/classes/class-a3-lazy | 0.0002s | 0s | 0 | | 0 | 0 | 0s | 0 |
| | -load.php:540 | | | | | | | | |
| tve_leads_query_group() | thrive-leads/inc/hooks.php:185 | 0.0614s | 0.021s | 30 | 93.1% | 216 | 16 | 0s | 0 |
| tve_leads_one_click_signup() | thrive-leads/inc/hooks.php:2614 | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| tve_wp_action() | thrive-visual-editor/plugin-core.p | 0.0006s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| | hp:204 | | | | | | | | |
| TCB_Editor->clean_inner_frame() | thrive-visual-editor/inc/classes/c | 0s | 0s | 0 | | 0 | 0 | 0s | 0 |
| | lass-tcb-editor.php:223 | | | | | | | | |
| Avada_Init->set_theme_version() | Avada/includes/class-avada-init.ph | 0.0017s | 0s | 0 | 64.29% | 9 | 5 | 0s | 0 |
| | p:101 | | | | | | | | |
| Avada_Dynamic_CSS->set_mode() | Avada/includes/class-avada-dynamic | 0.0003s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| | -css.php:62 | | | | | | | | |
| Avada->set_page_id() | Avada/includes/class-avada.php:239 | 0.0004s | 0s | 0 | 100% | 8 | 0 | 0s | 0 |
| Avada_Layout->add_sidebar() | Avada/includes/class-avada-layout. | 0.0251s | 0s | 0 | 100% | 516 | 0 | 0s | 0 |
| | php:20 | | | | | | | | |
| WPSEO_Frontend->page_redirect() | wordpress-seo/frontend/class-front | 0s | 0s | 0 | 100% | 1 | 0 | 0s | 0 |
| | end.php:1377 | | | | | | | | |
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (10) | | 0.0898s | 0.021s | 30 | 93.91% | 756 | 21 | 0s | 0 |
+----------------------------------+------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+Similar to the wp profile stage command, you can use the --all flag for hooks as well.
wp profile hook --all --spotlightYou can even simulate the loading for a specific URL on the site by using the --url flag.
wp profile hook --all --spotlight --url=https://guides.wp-bullet.com --orderby=time --allow-rootLooks like only my theme GeneratePress, Yoast and WP Theme Optimizer are being loaded.
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| callback | location | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| wpseo_init() | wordpress-seo/wp-seo-main.php:252 | 0.0136s | 0s | 0 | 100% | 34 | 0 | 0s | 0 |
| wpto_Public->wpto_remove_yoast_information() | wp-theme-optimizer/public/class-wpto-public.php:187 | 0.0151s | 0s | 0 | 100% | 18 | 0 | 0s | 0 |
| wp_enqueue_scripts() | wp-includes/script-loader.php:1294 | 0.015s | 0s | 0 | 100% | 348 | 0 | 0s | 0 |
| generate_construct_header() | generatepress/inc/template-tags.php:514 | 0.0041s | 0.0002s | 2 | 100% | 93 | 0 | 0s | 0 |
| generate_add_navigation_after_header() | generatepress/inc/navigation.php:23 | 0.0132s | 0.0003s | 3 | 100% | 367 | 0 | 0s | 0 |
| wp_trim_excerpt() | wp-includes/formatting.php:3289 | 0.0186s | 0s | 0 | 100% | 16 | 0 | 0s | 0 |
| generate_construct_sidebars() | generatepress/functions.php:434 | 0.0129s | 0.0001s | 1 | 100% | 178 | 0 | 0s | 0 |
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (7) | | 0.0927s | 0.0006s | 6 | 100% | 1054 | 0 | 0s | 0 |
+----------------------------------------------+-----------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+Let’s compare to my contact page
wp profile hook --all --spotlight --url=https://guides.wp-bullet.com/contact/ --orderby=time --allow-rootNow we can see Pretty Link is running, TablePress and Contact Form 7
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| callback | location | time | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| PrliAppController->redirect() | pretty-link/app/controllers/PrliAppController.php:295 | 0.0012s | 0.0003s | 1 | 100% | 2 | 0 | 0s | 0 |
| wpcf7() | contact-form-7/settings.php:88 | 0.012s | 0s | 0 | | 0 | 0 | 0s | 0 |
| wpseo_init() | wordpress-seo/wp-seo-main.php:252 | 0.023s | 0s | 0 | 100% | 34 | 0 | 0s | 0 |
| PrliAppController->install() | pretty-link/app/controllers/PrliAppController.php:253 | 0.014s | 0s | 0 | 100% | 2 | 0 | 0s | 0 |
| TablePress::run() | tablepress/classes/class-tablepress.php:101 | 0.0107s | 0s | 0 | 100% | 4 | 0 | 0s | 0 |
| wp_enqueue_scripts() | wp-includes/script-loader.php:1294 | 0.0247s | 0.0002s | 1 | 100% | 369 | 0 | 0s | 0 |
| generate_construct_header() | generatepress/inc/template-tags.php:514 | 0.0067s | 0.0003s | 2 | 100% | 97 | 0 | 0s | 0 |
| generate_add_navigation_after_header() | generatepress/inc/navigation.php:23 | 0.0219s | 0.0004s | 3 | 100% | 370 | 0 | 0s | 0 |
| generate_construct_sidebars() | generatepress/functions.php:434 | 0.0167s | 0.0002s | 1 | 100% | 178 | 0 | 0s | 0 |
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (9) | | 0.1309s | 0.0014s | 8 | 100% | 1056 | 0 | 0s | 0 |
+----------------------------------------+-------------------------------------------------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+You can look for the problematic callbacks with grep, generate_add_navigation_after_header isn’t problematic, I’m just using it as an example.
grep -ril generate_add_navigation_after_header wp-content/themesThis will spit out the files that contain the callback and you can examine them
wp-content/themes/generatepress/inc/navigation.phpIf you get multiple results you can make sort them so only unique (-u) are shown
grep -ril gettext wp-content/plugins/ | awk -F / '{ print $3}' | sort -uAnother useful grep command that developers can use is rHin which outputs the filename, line numer and before and after characters
grep -rHin generate_add_navigation_after_header wp-content/themesThe file with the occurrence is wp-content/themes/generatepress/inc/navigation.php
The line number is :17:
Where the string is found shows in pink
wp-content/themes/generatepress/inc/navigation.php:17:if ( ! function_exists( 'generate_add_navigation_after_header' ) ) :
wp-content/themes/generatepress/inc/navigation.php:22:add_action( 'generate_after_header', 'generate_add_navigation_after_header', 5 );
wp-content/themes/generatepress/inc/navigation.php:23:function generate_add_navigation_after_header()Now you can diagnose issues by running the wp profile commands before and after deactivating plugins too! Notice how overall times increase and find the slow code or plugin impairing your WordPress site
wp plugin deactivate plugin-nameAlternatively you can have WP-CLI simulate the deactivation of a plugin like this
wp profile hook --all --spotlight --skip-plugins=plugin-nameCompare the wp profile results before and after, happy WordPress performance troubleshooting :).
If you need help diagnosing the performance issues of your site get in touch with me via Codeable.
Sources
WP-CLI Profile Command – Github
WP-CLI Package Update – WP.org
WP Profile Command – EasyEngine
Identify WordPress Slowness – Daniel Bachhuber

The instructions for making a swap device when wp-cli aborts the package installation, didn’t worked for me, what solved my exhausted memory problem was edit php.ini rasing the memory_limit to 256 M
Also, looks like some lines were collapsed, unless you know what you are doing, pasting the commands won’t work. And it’s not resolved by making swap space, I have more than 4GB and still the sequence didn’t work.
Please force refresh the page, the pre and code tags are working now. You are right, will add the php.ini as well, thank you!
Nice! Now the instructions and profile results are easier to read. Thank you!
Thank you for pointing it out, hope this helps you in some way, have a great weekend 🙂
Instead of changing php.ini you can use php -d argument before like:
php -d memory_limit=256M $PATH_TO_WP/wp
In my case I had allow_url_fopen disabled for security and I had to add allow_url_fopen=1 to be able to install it
How can we profile in the admin to find performance issues?
Hi Mike,
I’m using the profile but it returns no values for cache_ratio, hits nor misses. I don’t know why as I am using LS Cache and it seems to be working properly. Any idea?
I am pretty sure the cache values you see in wp profile are not related to the page cache created by LS Cache but rather WordPress’s internal cache like transients and objects.
Thanks for the fast response. Do you know How can I check if the WordPress internal cache is working properly?