Top Tags

Limit posts in WordPress in admin area

Limit posts in WordPress in admin area

Overview

Limiting the number of posts displayed in the WordPress admin area is a critical performance optimization technique for large-scale WordPress installations. This approach reduces database queries, improves page load times, and enhances administrative usability by preventing the admin dashboard from becoming sluggish when dealing with thousands of posts.

Technical Context

Why Limit Posts in Admin?

  • Performance Impact: Large post lists require significant database resources. Loading 1000+ posts in the admin interface can cause timeouts and memory exhaustion.
  • UX Improvement: Pagination prevents administrators from accidentally loading massive datasets, making the dashboard more responsive.
  • Database Optimization: Reduces unnecessary wp_postmeta and wp_posts table queries, lowering server load.
  • Server Load: Reduces unnecessary queries on table joins and metadata lookups.

The pre_get_posts Hook - Official WordPress Documentation

According to WordPress Developer Reference, pre_get_posts fires after the WP_Query object is created but before the actual query is executed. This is the optimal point to modify query parameters.

Official Hook Specifications:

  • Source: wp-includes/class-wp-query.php
  • When it runs: After parse_query() but before database query execution
  • Parameters: WP_Query $query (passed by reference - modifications affect the query)
  • Since: WordPress 2.0.0

From official WordPress core:

php
1/**
2 * Fires after the query variable object is created, but before the actual query is run.
3 *
4 * Note: If using conditional tags, use the method versions within the passed instance
5 * (e.g. $this->is_main_query() instead of is_main_query()). This is because the functions
6 * like is_main_query() test against the global $wp_query instance, not the passed one.
7 *
8 * @since 2.0.0
9 * @param WP_Query $query The WP_Query instance (passed by reference).
10 */
11do_action_ref_array( 'pre_get_posts', array( &$this ) );

Critical Warning: Conditional Functions in pre_get_posts

⚠️ Important: According to WordPress documentation, some template tags and conditional functions will NOT work in pre_get_posts because they rely on WP_Query being fully initialized:

Functions that do NOT work:

  • is_front_page()
  • is_single()
  • is_archive()
  • is_page()

Functions that DO work:

  • is_home()
  • is_admin()
  • wp_get_current_user()

Solution: Use instance methods from the $query object instead:

php
1// ❌ Don't use global functions in pre_get_posts
2if ( is_main_query() ) { }
3
4// ✅ Use instance methods
5if ( $query->is_main_query() ) { }

Implementation

Basic Example

php
1function my_post_queries( $query ) {
2
3 if (is_admin() && $query->is_main_query()){
4
5
6 if(is_admin()){
7 $query->set('max_num_pages', 3);
8 }
9
10 if(is_category()){
11 $query->set('max_num_pages', 3);
12 }
13
14 }
15}
16add_action( 'pre_get_posts', 'my_post_queries' );

How It Works

  1. is_admin(): Global WordPress function that checks if the current request is from the WordPress admin area (not the front-end). This works in pre_get_posts.
  2. $query->is_main_query(): Instance method to ensure we only modify the main query, not secondary queries like sidebars, related posts, or custom loops. This is crucial to prevent unintended side effects.
  3. $query->set('max_num_pages', 3): Sets the maximum number of pages to 3, limiting initial post display.
  4. is_category(): Additional conditional to apply limits to specific query types.

Advanced Examples

Limit by Post Type

php
1function limit_posts_by_type( $query ) {
2 if ( is_admin() && $query->is_main_query() ) {
3 $post_type = get_current_screen()->post_type ?? 'post';
4
5 switch ( $post_type ) {
6 case 'post':
7 $query->set( 'posts_per_page', 10 );
8 break;
9 case 'page':
10 $query->set( 'posts_per_page', 20 );
11 break;
12 case 'attachment':
13 $query->set( 'posts_per_page', 50 );
14 break;
15 }
16 }
17}
18add_action( 'pre_get_posts', 'limit_posts_by_type' );

Limit by User Role

php
1function limit_posts_by_role( $query ) {
2 if ( is_admin() && $query->is_main_query() ) {
3 $user = wp_get_current_user();
4
5 // Editors see more posts than contributors
6 if ( in_array( 'editor', $user->roles ) ) {
7 $query->set( 'posts_per_page', 25 );
8 } elseif ( in_array( 'contributor', $user->roles ) ) {
9 $query->set( 'posts_per_page', 10 );
10 } elseif ( in_array( 'administrator', $user->roles ) ) {
11 $query->set( 'posts_per_page', 50 );
12 }
13 }
14}
15add_action( 'pre_get_posts', 'limit_posts_by_role' );

Include Custom Post Types in Results

php
1function include_custom_post_types( $query ) {
2 if ( ! is_admin() && $query->is_main_query() ) {
3 if ( $query->is_search() ) {
4 $query->set( 'post_type', array( 'post', 'page', 'my_custom_type' ) );
5 }
6 }
7}
8add_action( 'pre_get_posts', 'include_custom_post_types' );

Exclude Drafts from Admin View

php
1function exclude_drafts_from_admin( $query ) {
2 if ( is_admin() && $query->is_main_query() ) {
3 $query->set( 'posts_per_page', 15 );
4 $query->set( 'post_status', array( 'publish', 'pending' ) );
5 }
6}
7add_action( 'pre_get_posts', 'exclude_drafts_from_admin' );

Query Parameters Reference

ParameterTypePurposeExample
posts_per_pageIntegerNumber of posts per page10
pagedIntegerCurrent page number1, 2, 3
orderbyStringSort by: date, ID, title, modified, rand'date'
orderStringASC (ascending) or DESC (descending)'DESC'
post_typeString/ArrayPost type(s) to query'post', ['post', 'page']
post_statusString/ArrayPost status filter'publish', ['publish', 'pending']
sStringSearch term'query text'
meta_keyStringCustom field key'_featured'

WP_Query Execution Flow

According to official WordPress documentation, here's the execution sequence when pre_get_posts is called:

  1. parse_query() - Parses and initializes query variables
  2. pre_get_posts hook fires ← Your modifications happen here
  3. fill_query_vars() - Fills any unset query variables with defaults
  4. WP_Meta_Query initialization - Prepares meta query parsing
  5. SQL query execution - Database call is made

Performance & Pagination Warnings

⚠️ Using Offset Breaks Pagination

From WordPress Developer Reference Documentation:

Using the offset argument in any WordPress query can break pagination. If you need to use offset and preserve pagination, you will need to handle pagination manually.

The reason is that offset is applied after the LIMIT clause, which confuses WordPress's pagination system.

Avoid this pattern:

php
1$query->set( 'offset', 10 ); // ❌ Breaks pagination

Use paging instead:

php
1$query->set( 'paged', get_query_var( 'paged' ) ); // ✅ Proper pagination

Query Performance Best Practices

  1. Combine multiple pre_get_posts hooks into a single callback to reduce hook overhead
  2. Use early returns to exit the function early and avoid unnecessary processing
  3. Avoid nested WP_Query calls within the callback - use get_posts() instead if needed
  4. Monitor with Query Monitor plugin - Identify and optimize slow queries
  5. Index database columns - Ensure post_type, post_status, and post_date are indexed

Targeting the Right Query

According to WordPress documentation:

Be aware of the queries you are changing when using the pre_get_posts action. Make use of conditional tags to target the right query. For example, it's recommended to use the is_admin() conditional to not change queries in the admin screens. With the $query->is_main_query() conditional from the query object you can target the main query of a page request.

Debugging & Testing

Log Query Parameters

php
1function debug_query_parameters( $query ) {
2 if ( is_admin() && $query->is_main_query() ) {
3 error_log( 'Posts per page: ' . $query->get( 'posts_per_page' ) );
4 error_log( 'Paged: ' . $query->get( 'paged' ) );
5 error_log( 'Post type: ' . implode( ',', (array) $query->get( 'post_type' ) ) );
6 error_log( 'SQL Query: ' . $query->request );
7 }
8}
9add_action( 'pre_get_posts', 'debug_query_parameters', 99 );

Test with Query Monitor

Install the Query Monitor plugin to:

  • See actual SQL queries generated
  • Identify which hooks are modifying queries
  • Spot performance bottlenecks
  • Monitor database query count and execution time

Common Issues & Solutions

Issue: Limits Not Applied

Cause: Not checking $query->is_main_query(), causing secondary queries to be affected.

Solution:

php
1if ( is_admin() && $query->is_main_query() ) {
2 // Only modify the main query
3 $query->set( 'posts_per_page', 15 );
4}

Issue: Admin Search Not Working

Cause: Query modifications interfering with admin search functionality.

Solution: Skip modification when search is active:

php
1if ( is_admin() && $query->is_main_query() && ! isset( $_GET['s'] ) ) {
2 $query->set( 'posts_per_page', 15 );
3}

Issue: Conditional Tags Return Wrong Values

Cause: Using global conditional functions instead of instance methods in pre_get_posts.

Solution: Use instance methods on the $query object:

php
1// ❌ Won't work as expected in pre_get_posts
2if ( is_single() ) { /* ... */ }
3
4// ✅ Correct - use instance methods
5if ( $query->is_single() ) { /* ... */ }

Issue: Filter Applied to Wrong Query

Cause: No conditional check for is_main_query().

Solution: According to WordPress docs, always target the right query:

php
1// Only modify the main query, not secondary/nested queries
2if ( ! is_admin() && $query->is_main_query() ) {
3 $query->set( 'posts_per_page', 15 );
4}

References