The WordPress REST API was merged into WordPress core in version 4.7. Before that, developers relied on the default AJAX implementation, otherwise known as admin-ajax
after the /wp-admin/admin-ajax.php
file that processes AJAX requests in WordPress.
Since the introduction of the WordPress REST API, many plugin developers have started converting their plugins to use the REST API instead of AJAX. Aside from being newer technology, the REST API is considered a better option because less of WordPress core is loaded during a typical REST request, and a REST response is always in a predictable format based on its schema. However, is it faster than an AJAX request? Is there another option if the raw performance of your asynchronous requests is critical?
In this article, we’ll compare the pros and cons of each approach. We’ll also run some benchmarks to see which is faster, as well as look at how you can speed up your request processing.
There’s Life in the Old Dog Yet: AJAX
AJAX support in WordPress, in the form of the admin-ajax.php
file, was added in 2006 as a way to process a few admin UI functions without annoying page reloads. It was also heavily used by WordPress plugin and theme developers as a way to make asynchronous requests in a WordPress site. When a typical AJAX request to admin-ajax.php
is made, it loads a few other core WordPress files to make sure the core functions are loaded.
/wp-load.php
/wp-config.php
/wp-settings.php
(loads most core files, all active plugins and themes, and the REST API)/wp-admin/includes/admin.php
/wp-admin/includes/ajax-actions.php
After loading these files, WordPress calls the admin_init
hook, which several core functions hook into. The below core functions are registered on this hook in WordPress 6.3:
handle_legacy_widget_preview_iframe
wp_admin_headers
default_password_nag_handler
WP_Privacy_Policy_Content::text_change_check
WP_Privacy_Policy_Content::add_suggested_content
register_setting
add_privacy_policy_content
send_frame_options_header
register_admin_color_schemes
_wp_check_for_scheduled_split_terms
_wp_check_for_scheduled_update_comment_type
_wp_admin_bar_init
wp_schedule_update_network_counts
_maybe_update_core
_maybe_update_plugins
_maybe_update_themes
After these functions have been called, WordPress finally calls the AJAX action provided in the $_GET[‘action’]
or $_POST[‘action’]
variable. This triggers a callback function, retrieves the relevant WordPress data, and serves it in the response.
The New Kid on the Block: WP REST API
While relatively new to the WordPress development ecosystem, the WordPress REST API can be used for a number of implementations other than asynchronous requests, from static site generators to headless single-page apps.
A typical REST API request looks slightly different from a request to admin-ajax.php
. Since the REST endpoints are handled by the WordPress Rewrite API, the request is passed to /index.php
, which then loads the rest of WordPress normally.
/index.php
/wp-blog-header.php
/wp-load.php
/wp-config.php
/wp-settings.php
(loads most core files, all active plugins and themes, and the REST API)
Unlike requests sent over admin-ajax.php
, the REST API doesn’t load the WordPress admin section via /wp-admin/includes/admin.php
, nor does it fire the admin_init
action hook. Based on that, it would seem that any plugins or themes that don’t rely on admin-specific functionality—but are making asynchronous requests using admin-ajax.php
—should see a slight performance boost by switching over to the REST API.
REST API Adoption in the WordPress Ecosystem
Since the inclusion of the REST API in WordPress, it has been largely adopted by WordPress Core, as well as theme and plugin developers.
The most popular usage of the REST API has been in the development of the WordPress Block Editor. Building custom blocks for the editor relies heavily on using the REST API through the api-fetch
package.
The REST API and the api-fetch
package also open up WordPress to a wider variety of use cases. Developers who work with popular frontend JavaScript frameworks can build WordPress themes or great looking plugin interfaces without needing to rely on PHP for templating. Not only that, but it gives WordPress the ability to power single-page applications and mobile apps, something that was not easily possible before.
Finally, it gives WordPress plugin developers a standardized way to work with custom plugin data. For example, WooCommerce has fully integrated with the WordPress REST API. This allows WooCommerce data like products and orders to be created, read, updated, and deleted using the WP REST API request formats and authentication methods. Before this integration, developers would have had to build their own custom endpoints to access this data externally.
The Dark Horse: Must Use Plugin
As with most cases in software development, there are multiple ways to solve a problem. While admin-ajax.php
and the REST API are solid choices, building a custom endpoint also has its own advantages. The biggest of these is performance.
There are a few different ways to set up custom endpoints in a WordPress website. The fastest possible endpoint would likely be completely independent of WordPress. It could be as simple as a standalone PHP file that is uploaded to the web server and processes a specific GET request. While fast, it probably wouldn’t be all that helpful, because it won’t have access to any WordPress core functions. Access to these functions often saves a great deal of development time.
A better approach would be to create a custom endpoint in a must-use plugin to process custom asynchronous HTTP requests. That way it can respond to any custom requests after WordPress core loads, but before the plugins and themes are loaded. This would both provide the best performance and prevent conflicts from other plugins that are using the REST API or admin-ajax.php
.
Below is a simple example of a must-use plugin that expects a GET or POST variable hfm-ajax
, and outputs the value of time()
in the response.
<?php
/**
* Plugin Name: Hellfish Media Custom AJAX
* Description: Custom Request Handler
* Author: Hellfish Media
* Version: 1.0
* Author URI: https://hellfish.media
*/
if ( ! isset( $_GET['hfm-ajax'] ) ) {
return;
}
// Define the WordPress "DOING_AJAX" constant.
if ( ! defined( 'DOING_AJAX' ) ) {
define( 'DOING_AJAX', true );
}
wp_send_json(
array(
'time' => time()
)
);
If I installed this on the https://hellfish.media
site, it could be accessed at https://hellfish.media/?hfm-ajax=1
.
Creating the Benchmarks
The must-use plugin functionality is perfect for our benchmark test, so all we need to do is replicate its functionality for admin-ajax.php
and the REST API. First, we’ll create the benchmark function, which returns the time
value in the response. This makes it easier to see that the requests aren’t being cached.
function hfm_benchmark_request() {
wp_send_json(
array(
'time' => time()
)
);
}
To register this function for admin-ajax.php
, we need to hook the function into a wp_ajax_nopriv_{$action}
hook.
add_action( 'wp_ajax_nopriv_benchmark_request', 'hfm_benchmark_request' );
We’re using a wp_ajax_nopriv_{$action}
hook so that we don’t need to be logged in to test the admin-ajax.php
request.
To register this function for the REST API, we hook it into the rest_api_init
hook and set up a custom REST API route.
add_action( 'rest_api_init', 'hfm_register_custom_rest_route');
function hfm_register_custom_rest_route() {
register_rest_route( 'benchmark/v1', '/benchmark', array(
'methods' => GET,
'callback' => 'hfm_benchmark_rest_request',
'permission_callback' => '__return_true',
) );
}
function hfm_benchmark_rest_request() {
return array( 'time' => time() );
}
We can put all this code in a custom plugin or the functions.php
of a theme or child theme. For the purposes of the benchmark, I’m going to be working on a 1GB/1vCPU Digital Ocean droplet running WordPress 5.9, with no additional plugins, a default theme, and page caching disabled.
Running the Numbers
To perform the actual benchmarking, we’re going to use ApacheBench, a command-line benchmarking tool that allows you to fire off multiple requests at once to get a feel for how the server is performing. To make sure I was testing valid requests, I also verified the output of each URL tested in Postman.
I’ll create an apachebench
directory to run the tests and save the output to a file.
mkdir ~/apachebench
cd apachebench
Let’s test the admin-ajax.php
version first. In the case of the AJAX request, we pass a query string including an action
variable with a value of benchmark_request
, which matched the $action
value of the wp_ajax_nopriv_
hook defined in the test code.
ab -n 100 -c 1 -g ajax.tsv \
https://dev.hellfish.media/wp-admin/admin-ajax.php?action=benchmark_request
The above command sends 100 GET requests to the /wp-admin/admin-ajax.php
file and logs the response times.
➜ ~ ab -n 100 -c 1 -g ajax.tsv \ https://dev.hellfish.media/wp-admin/admin-ajax.php\?action\=benchmark_request This is ApacheBench, Version 2.3 <$Revision: 1903618 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking dev.hellfish.media (be patient).....done Server Software: nginx/1.25.2 Server Hostname: dev.hellfish.media Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256 Server Temp Key: ECDH X25519 253 bits TLS Server Name: dev.hellfish.media Document Path: /wp-admin/admin-ajax.php?action=benchmark_request Document Length: 19 bytes Concurrency Level: 1 Time taken for tests: 7.028 seconds Complete requests: 100 Failed requests: 0 Total transferred: 43900 bytes HTML transferred: 1900 bytes Requests per second: 14.23 [#/sec] (mean) Time per request: 70.280 [ms] (mean) Time per request: 70.280 [ms] (mean, across all concurrent requests) Transfer rate: 6.10 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 6 11 16.8 10 177 Processing: 56 59 5.8 58 114 Waiting: 56 59 5.8 58 114 Total: 64 70 22.4 68 291 Percentage of the requests served within a certain time (ms) 50% 68 66% 69 75% 69 80% 69 90% 70 95% 71 98% 73 99% 291 100% 291 (longest request)
With 100 requests, the average response time was 72.28ms. This gives a good baseline for the same test over the REST API, so let’s test that next.
ab -n 100 -c 1 -g rest.tsv \
https://dev.hellfish.media/wp-json/benchmark/v1/benchmark
➜ ~ ab -n 100 -c 1 -g rest.tsv \ https://dev.hellfish.media/wp-json/benchmark/v1/benchmark This is ApacheBench, Version 2.3 <$Revision: 1903618 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking dev.hellfish.media (be patient).....done Server Software: nginx/1.25.2 Server Hostname: dev.hellfish.media Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256 Server Temp Key: ECDH X25519 253 bits TLS Server Name: dev.hellfish.media Document Path: /wp-json/benchmark/v1/benchmark Document Length: 19 bytes Concurrency Level: 1 Time taken for tests: 6.085 seconds Complete requests: 100 Failed requests: 0 Total transferred: 53100 bytes HTML transferred: 1900 bytes Requests per second: 16.43 [#/sec] (mean) Time per request: 60.851 [ms] (mean) Time per request: 60.851 [ms] (mean, across all concurrent requests) Transfer rate: 8.52 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 6 10 5.2 10 60 Processing: 47 51 6.1 49 106 Waiting: 47 50 6.1 49 105 Total: 54 61 11.0 59 166 Percentage of the requests served within a certain time (ms) 50% 59 66% 60 75% 61 80% 61 90% 63 95% 66 98% 71 99% 166 100% 166 (longest request)
Not surprisingly, the REST API is slightly faster in this comparison, with an average response time of 60.85ms over 100 requests. In these tests, the REST API is about 17.3% faster than the AJAX API.
But what about the must-use plugin?
ab -n 100 -c 1 -g custom-ajax.tsv https://dev.hellfish.media/?hfm-ajax=1
hellfishdev@dev:~/apachebench$ ab -n 100 -c 1 -g custom-ajax.tsv \ > https://dev.hellfish.media/?hfm-ajax=1 This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking dev.hellfish.media (be patient).....done Server Software: nginx Server Hostname: dev.hellfish.media Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256 Server Temp Key: X25519 253 bits TLS Server Name: dev.hellfish.media Document Path: /?hfm-ajax=1 Document Length: 19 bytes Concurrency Level: 1 Time taken for tests: 0.560 seconds Complete requests: 100 Failed requests: 0 Total transferred: 43100 bytes HTML transferred: 1900 bytes Requests per second: 178.52 [#/sec] (mean) Time per request: 5.602 [ms] (mean) Time per request: 5.602 [ms] (mean, across all concurrent requests) Transfer rate: 75.14 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 2 2 0.2 2 3 Processing: 3 4 0.3 4 5 Waiting: 3 4 0.3 3 5 Total: 5 6 0.4 5 7 Percentage of the requests served within a certain time (ms) 50% 5 66% 6 75% 6 80% 6 90% 6 95% 6 98% 7 99% 7 100% 7 (longest request)
As expected, this is faster than both the AJAX and REST APIs. The average time for the custom request handler is 5.49ms, which is blisteringly fast compared to the other options.
To validate this data, I decided to run the same benchmarks with some content, a few plugins, and a custom theme activated. I used WP Migrate to make a copy of our https://hellfish.media
demo site, which has some demo content, a custom theme, and a bunch of active plugins, including:
- Advanced Custom Fields PRO
- Advanced Forms
- WP DB Backup
- EWWW Image Optimizer
- Simple Custom Post Order
- WordPress Importer
- WP Migrate
- WP Offload Media Lite
- WP Pusher
The average request times are listed below:
- admin-ajax: 92.4ms
- REST API: 89.47ms
- custom request handler: 6.57ms
The first thing you notice is the overall increase in both the admin-ajax.php
and REST API request times compared to the must-use plugin. Both admin-ajax.php
and REST API requests are much slower, but the must-use plugin is just 1ms slower. The second thing you’ll notice is that the gap in performance between the REST API and admin-ajax.php
has been reduced to just a few milliseconds.
A website with a different set of plugins, or a different theme, could see a different set of results, but that depends completely on the theme and plugins running on the site, and how they are coded.
How Plugins Affect Asynchronous Request Performance
Every time an admin-ajax.php
or REST API request is served by WordPress, most of WordPress core, the active theme, and all the active plugins are loaded. We experienced this firsthand when we developed WP Migrate. In early versions, plugins conflicting with migrations made up the majority of our support requests. Some plugins came up so often that we started publishing a list of plugins known to conflict with WP Migrate.
This was not an ideal solution, so we developed Compatibility Mode.
Compatibility Mode allows the user to select which plugins to load for requests made by WP Migrate. By default, all plugins are prevented from loading. This greatly reduces the chance of another plugin disrupting the migration process.
Compatibility Mode installs a must-use plugin that hooks into the option_active_plugins
filter whenever get_option('active_plugins')
is called, and executes a function which may modify which plugins are active.
You can make use of the same principle to prevent plugins from loading when running asynchronous requests. Below is an example of a must-use plugin that could be used to exclude specific plugins from an AJAX request.
/**
* Plugin Name: Hellfish Media Exclude plugins
* Description: Exclude plugins from ajax requests
* Author: Hellfish Media
* Version: 1.0
* Author URI: https://hellfish.media
*/
/**
* Plugin Name: Hellfish Media Exclude plugins
* Description: Exclude plugins from ajax requests
* Author: Hellfish Media
* Version: 1.0
* Author URI: https://hellfish.media
*/
add_filter( 'option_active_plugins', 'hfm_exclude_plugins' );
function hfm_exclude_plugins( $plugins ) {
/**
* If we're not performing our AJAX request, return early.
*/
if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX || ! isset( $_REQUEST['action'] ) || 'benchmark_request' !== $_REQUEST['action'] ) {
return $plugins;
}
/**
* The list of plugins to exclude. Flip the array to make the check that happens later possible
*/
$denylist_plugins = array_flip(
array(
'advanced-custom-fields-pro/acf.php',
'advanced-forms/advanced-forms.php',
'classic-editor/classic-editor.php',
'wp-db-backup/wp-db-backup.php',
'ewww-image-optimizer/ewww-image-optimizer.php',
'limit-login-attempts-reloaded/limit-login-attempts-reloaded.php',
'simple-custom-post-order/simple-custom-post-order.php',
'theme-switcha/theme-switcha.php',
'woocommerce/woocommerce.php',
'wordpress-importer/wordpress-importer.php',
'wp-mail-log/wp-mail-log.php',
'amazon-s3-and-cloudfront/wordpress-s3.php',
'wp-offload-ses/wp-offload-ses.php',
'wppusher/wppusher.php'
)
);
/**
* Loop through the active plugins, if it's not in the deny list, allow the plugin to be loaded
* Otherwise, remove it from the list of plugins to load
*/
foreach ( $plugins as $key => $plugin ) {
if ( ! isset( $denylist_plugins[ $plugin ] ) ) {
continue;
}
unset( $plugins[ $key ] );
}
return $plugins;
}
You have two options if you want to exclude a list of plugins that are loaded for a REST API request. If the REST API endpoint is a WordPress core endpoint, you can check if the REST_REQUEST
constant is defined and set to true. This constant is defined in the rest_api_loaded
core function. So instead of the DOING_AJAX
checks, you could do the following:
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
return $plugins;
}
If it’s a custom endpoint registered using register_rest_route
(the same as we used in our benchmarks), it doesn’t seem possible to use this constant. Instead, we can get the REQUEST_URI
variable in the PHP $_SERVER
predefined constant, and see if it matches our custom endpoint.
$request_uri = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
if ( '/wp-json/benchmark/v1/benchmark/' !== trailingslashit( $request_uri ) ) {
return $plugins;
}
With those changes implemented, you should see a reduction in response times that correlates with the number of plugins that were prevented from loading.
Should You Use the WordPress REST API?
In addition to a slight performance increase, creating endpoints for other developers to use is less complicated with the REST API. When we added REST API integration for Advanced Custom Fields, we appreciated that we could leverage the REST standards, schema, and authentication methods, and just register ACF field data on the defined REST API endpoints. The REST API is also better documented than using admin-ajax.php
, and therefore more straightforward to implement.
While admin-ajax.php
and a custom request handler can require more work to get the data you want to return, they also give you more flexibility in how you format that data. And as we pointed out, a custom request handler is the obvious choice if raw performance is what you’re after.
In terms of reliability, the REST API and admin-ajax.php
still depend on the quality and integrity of the active plugins or themes. A poorly coded plugin could still easily interfere with REST API or admin-ajax.php
requests, and the likelihood of this increases with the number of plugins installed on your site.
Overall, it’s definitely a good idea to at least consider using the REST API. Adding custom API endpoints is relatively straightforward compared to admin-ajax.php
, and it doesn’t take much to switch over existing code either.
Have you converted your admin-ajax requests over to the REST API? Did you see performance increase, decrease, or not change at all? Let us know in the comments below.