The WordPress REST API offers incredible flexibility for headless setups, mobile apps, and custom admin interfaces. However, without proper optimization, API requests can become sluggish, especially as your site grows.
In this post, I’ll share techniques to reduce response time and payload size for WordPress REST API endpoints based on real-world implementations.
Understanding REST API Performance Bottlenecks
Before diving into optimizations, let’s identify common REST API performance issues:
- Excessive data retrieval – Fetching more fields than needed
- Unnecessary nested data – Requesting embedded resources that aren’t used
- Inefficient queries – Poor database queries behind the scenes
- Authentication overhead – Complex authentication checking on every request
- Missing caching layers – Not utilizing available caching mechanisms
1. Limit Response Fields
One of the simplest optimizations is to request only the fields you need using the _fields
parameter:
/wp-json/wp/v2/posts?_fields=id,title,excerpt
Before: ~15KB per post with 20+ fields After: ~2KB per post with just 3 fields
This significantly reduces JSON parsing time and network transfer.
For custom endpoints, you can implement field filtering:
add_filter( 'rest_prepare_post', 'my_rest_prepare_post', 10, 3 );
/**
* Filter the post data given in the REST API.
*
* This filter is required to only expose the fields that are requested via the `_fields` parameter in the URL.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post The post object.
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The filtered response object.
*/
function my_rest_prepare_post( $response, $post, $request ) {
$fields = $request->get_param( '_fields' );
if ( ! $fields ) {
return $response;
}
$fields = explode( ',', $fields );
$data = $response->get_data();
$filtered_data = array();
foreach ( $fields as $field ) {
if ( isset( $data[ $field ] ) ) {
$filtered_data[ $field ] = $data[ $field ];
}
}
$response->set_data( $filtered_data );
return $response;
}
2. Optimize _embed Requests
The _embed
parameter fetches related resources, but can be expensive. Instead of using the full embed, specify only what you need:
/wp-json/wp/v2/posts?_embed=author,wp:featuredmedia
If you’re building custom endpoints, control embedded data carefully:
register_rest_field( 'post', 'author_info', [
'get_callback' => function ( $post ) {
$author = get_user_by( 'id', $post['author'] );
return [
'name' => $author->display_name,
'avatar' => get_avatar_url( $author->ID, [ 'size' => 96 ] )
];
}
] );
This custom field is much more efficient than embedding the full author object.
3. Implement Custom Caching
WordPress doesn’t cache REST API responses by default. Add a custom caching layer:
add_filter( 'rest_post_dispatch', 'cache_rest_api_response', 10, 3 );
add_filter( 'rest_pre_dispatch', 'get_cached_rest_api_response', 10, 3 );
/**
* Cache REST API responses.
*
* This function caches the REST API response for a given route and query string.
* The cache is stored in a transient with a key that is an MD5 hash of the route and query string.
* The cache expiration is set to 5 minutes by default, but can be overridden for specific endpoints.
* For example, the `/wp/v2/posts` endpoint is cached for 1 hour.
*
* @param mixed $response The REST API response.
* @param object $handler The REST API handler.
* @param object $request The REST API request.
*
* @return mixed The filtered response.
*/
function cache_rest_api_response( mixed $response, object $handler, object $request ):mixed {
if ( ! is_wp_error( $response ) ) {
$cache_key = 'rest_' . md5( $request->get_route() . '?' . $_SERVER['QUERY_STRING'] );
// Set cache expiration based on endpoint.
$cache_time = 300; // 5 minutes default.
if ( str_contains( $request->get_route(), '/wp/v2/posts' ) ) {
$cache_time = 3600; // 1 hour for posts.
}
set_transient( $cache_key, $response, $cache_time );
}
return $response;
}
/**
* Retrieves a cached REST API response.
*
* This function is a filter on the `rest_pre_dispatch` hook.
* It checks if a transient exists for the given request route and query string.
* If a transient exists, it is returned instead of the actual response.
*
* @param mixed $response The original response.
* @param object $handler The REST API handler.
* @param object $request The REST API request.
*
* @return mixed The cached response if it exists, otherwise the original response.
*/
function get_cached_rest_api_response( mixed $response, object $handler, object $request ): mixed {
$cache_key = 'rest_' . md5( $request->get_route() . '?' . $_SERVER['QUERY_STRING'] );
$cached_response = get_transient( $cache_key );
if ( $cached_response ) {
return $cached_response;
}
return $response;
}
4. Use Collection Pagination Efficiently
Large collections can cause significant slowdowns. Always use pagination:
/wp-json/wp/v2/posts?per_page=10&page=2
For client applications, implement infinite scroll or “load more” instead of requesting all items at once.
You can also optimize the pagination headers with:
add_filter('rest_post_dispatch', 'optimize_collection_count', 10, 3);
/**
* Optimize collection count for large datasets.
*
* If we have more than 1000 posts, use an estimate for the total count.
*
* @param \WP_REST_Response $response The response object.
* @param \WP_REST_Server $handler The response handler.
* @param \WP_REST_Request $request The request object.
*
* @return \WP_REST_Response The modified response.
*/
function optimize_collection_count( \WP_REST_Response $response, \WP_REST_Server $handler, \WP_REST_Request $request ): \WP_REST_Response {
// Only run on collection endpoints.
if ( ! empty( $response->get_matched_route() ) &&
str_contains( $response->get_matched_route(), '/wp/v2/posts' ) ) {
// Get the total count from the headers
$total = $response->get_headers()['X-WP-Total'];
// If we have many posts, use an estimate.
if ( $total > 1000 ) {
$response->header( 'X-WP-Total', '1000+' );
$response->header( 'X-WP-TotalPages', ceil( 1000 / $request['per_page'] ) );
}
}
return $response;
}
This prevents expensive COUNT(*)
queries when you have many posts.
5. Optimize Custom Endpoint Queries
For custom endpoints, ensure your database queries are optimized:
register_rest_route( 'my-api/v1', '/popular-posts', [
'methods' => 'GET',
'callback' => 'get_popular_posts',
'permission_callback' => '__return_true'
] );
/**
* Retrieves an array of popular posts ordered by view count.
*
* @return array
*/
function get_popular_posts(): array {
global $wpdb;
// Use a more efficient query than WP_Query for specific cases.
$posts = $wpdb->get_results(
$wpdb->prepare(
"SELECT p.ID, p.post_title, pm.meta_value as view_count
FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_status = 'publish'
AND p.post_type = 'post'
AND pm.meta_key = 'view_count'
ORDER BY pm.meta_value+0 DESC
LIMIT %d",
10
)
);
return array_map( function ( $post ) {
return [
'id' => $post->ID,
'title' => $post->post_title,
'views' => $post->view_count,
'link' => get_permalink( $post->ID )
];
}, $posts );
}
This direct query is much faster than using WP_Query
for this specific use case.
6. Implement Server-Side Request Caching
Add object caching to prevent duplicate database queries:
/**
* Retrieve posts from the database with caching.
*
* This function wraps the built-in `WP_Query` class with caching to improve performance.
* It takes a `WP_REST_Request` object as a parameter and returns an array of associative
* arrays containing the post ID, title, and other fields.
*
* The cache key is generated by serializing the request parameters and taking the MD5 hash.
* The cache is stored for 5 minutes.
*
* @param \WP_REST_Request $request The request object.
*
* @return array An array of associative arrays containing the post data.
*/
function get_posts_with_cache( \WP_REST_Request $request ): array {
$cache_key = 'api_posts_' . md5( serialize( $request->get_params() ) );
$posts = wp_cache_get( $cache_key, 'api' );
if ( false === $posts ) {
$query = new \WP_Query( [
'post_type' => 'post',
'posts_per_page' => $request['per_page'] ?? 10,
'paged' => $request['page'] ?? 1,
// Other parameters...
] );
$posts = array_map( function ( $post ) {
return [
'id' => $post->ID,
'title' => $post->post_title,
// Other fields...
];
}, $query->posts );
wp_cache_set( $cache_key, $posts, 'api', 300 ); // Cache for 5 minutes
}
return $posts;
}
7. Use External Caching and CDNs
For high-traffic sites, implement external caching:
add_filter( 'rest_post_dispatch', 'add_cache_control_headers' );
/**
* Adds Cache-Control header to API responses.
*
* This filter adds a Cache-Control header with a max-age of 60 seconds to API responses.
* This allows the browser to cache API requests for 1 minute, improving performance.
*
* @param \WP_HTTP_Response|\WP_Error $response The response object or error.
*
* @return \WP_HTTP_Response|\WP_Error The modified response object.
*/
function add_cache_control_headers( \WP_HTTP_Response|\WP_Error $response ): \WP_HTTP_Response|\WP_Error {
if ( ! is_wp_error( $response ) ) {
$response->header( 'Cache-Control', 'public, max-age=60' ); // 1 minute
}
return $response;
}
This helps CDNs and browsers cache your API responses.
8. Optimize Authentication
REST API authentication can add overhead. If you’re building a public-facing API, consider:
add_filter( 'rest_authentication_errors', 'optimize_api_auth', 5, 3 );
/**
* Only run authentication when needed
*
* This function is used as a filter for `rest_authentication_errors`. It allows
* the API to skip authentication for GET requests to specific endpoints.
*
* @param mixed $result The value to return instead of the HTTP request.
* @param object $server The server object.
* @param object $request The request object.
*
* @return mixed The filtered value.
*/
function optimize_api_auth( mixed $result, object $server, object $request ): mixed {
// Skip authentication for GET requests to specific endpoints
if ( $request->get_method() === 'GET' &&
str_contains( $request->get_route(), '/wp/v2/posts' ) ) {
// Return true to indicate that authentication should be skipped.
return true;
}
return $result;
}
9. Benchmark and Compare
Before and after implementing these optimizations, benchmark your API:
# Before optimization
time curl -s https://yourdomain.com/wp-json/wp/v2/posts
# After optimization
time curl -s https://yourdomain.com/wp-json/wp/v2/posts?_fields=id,title,excerpt
Conclusion
The WordPress REST API is powerful but needs optimization for production use. By implementing these techniques, you can significantly improve performance, reduce server load, and create a better experience for your users.