Posts per page lives in Settings, Reading. If page 2 returns a 404, you almost certainly have a custom WP_Query on a page that also runs the main query, and the two disagree about what page you are on.
Pagination in WordPress is one setting and one recurring bug. The setting takes ten seconds. The bug — page one looks perfect, page two 404s — accounts for most of the searching people do on this topic, and the cause is consistent enough to be worth stating up front.
Table of contents
- The setting
- Changing it for one query only
- Why page 2 gives a 404
- Outputting the links
- The SEO side
- Pagination and caching
- How this fits the rest of the stack
- FAQ
The setting
Settings, Reading, “Blog pages show at most”. That value governs the main query on the blog index, archives, categories, tags and search results.
There is no universally correct number, but the trade is real. Fewer posts per page means faster loads and more pagination clicks; more means fewer clicks and a heavier page, particularly with featured images. Somewhere between 6 and 12 suits most sites, and a grid layout usually wants a multiple of the column count so the last row is not ragged.
The RSS feed has its own separate setting on the same screen — “Syndication feeds show the most recent” — and changing one does not affect the other. Feed readers generally want more items than a web page does.
Changing it for one query only
To use a different count on, say, category archives without touching the global setting, the correct hook is pre_get_posts:
add_action( 'pre_get_posts', function ( $query ) {
// Admin screens and secondary queries must be left alone.
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_category() ) {
$query->set( 'posts_per_page', 12 );
}
if ( $query->is_search() ) {
$query->set( 'posts_per_page', 20 );
}
} );
The two guards on the first check are both necessary. Without is_admin(), you change the post list in the dashboard. Without is_main_query(), you change every secondary query on the page including widgets and related-post loops.
This is the correct approach because it modifies the main query rather than replacing it, so pagination, canonical URLs and the $wp_query global all stay consistent. Which is exactly what the common bug gets wrong.
Why page 2 gives a 404
Here is the pattern, in thousands of themes:
// BROKEN -- page 2 will 404
$posts = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 6,
) );
Two things are wrong. The query has no paged parameter, so it returns the first 6 posts on every page. And meanwhile the main query — which WordPress uses to decide whether the requested page exists — is still using the Settings value.
So on /page/3/, if the main query finds no posts at offset 20 with its own settings, WordPress issues a 404 before your custom loop’s output matters.
// Works
$paged = max( 1, get_query_var( 'paged' ) ?: get_query_var( 'page' ) );
$posts = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 6,
'paged' => $paged,
) );
Note the two query vars. paged is used on archives and the blog index; page is used on a static front page. Checking both is why that line looks redundant and is not.
But the better fix is usually to not run a custom query at all. If you are displaying the main list of posts for the page, modify the main query with pre_get_posts and use the default loop. The 404 problem disappears because there is only one query to be consistent with.
Outputting the links
For the main query, the modern function is the_posts_pagination(), which produces accessible numbered links:
the_posts_pagination( array(
'mid_size' => 2,
'prev_text' => __( 'Previous', 'textdomain' ),
'next_text' => __( 'Next', 'textdomain' ),
) );
For a custom query, the pagination functions read the global $wp_query, so you must tell them about yours:
echo paginate_links( array(
'total' => $posts->max_num_pages,
'current' => $paged,
'mid_size' => 2,
) );
wp_reset_postdata(); // always, after a custom loop
wp_reset_postdata() is not optional. Without it, the global post object stays pointed at the last post of your custom loop, and everything after it on the page — the sidebar, the footer, template tags — reads the wrong post.
Older themes use next_posts_link() and previous_posts_link(). They still work and produce a worse experience: no page numbers, and no sense of how much there is.
The SEO side
Paginated archives raise real questions, and the answers have changed over the years:
rel=nextandrel=prevare no longer used by Google as an indexing signal. There is no harm in emitting them; there is no benefit either.- Do not canonicalise page 2 to page 1. That tells search engines the pages are duplicates, and the posts listed only on page 2 lose their internal link. Each paginated page should be self-canonical.
- Do not noindex paginated pages unless you have a specific reason. They are a legitimate crawl path to older content.
- Do differentiate the titles.
Blog - Page 2rather thanBlogon both, so search results are not duplicated.
The practical issue with deep pagination is crawl depth: a post on page 40 is 40 clicks from the front page, and gets crawled accordingly. Categories, tags and a decent search reduce how much your archive depends on pagination as its only navigation.
Pagination and caching
One operational note. Every paginated URL is a separate page to cache, so a blog with 50 pages of archives multiplied across categories and tags generates a large number of cacheable URLs — and they all invalidate when a new post shifts everything by one position.
That is manageable and worth knowing when a cache hit rate looks lower than expected on a content-heavy site. Reducing posts per page increases the number of pages; increasing it makes each page heavier. There is no free option, only a trade to make deliberately.
Managed WordPress on RunxBuild runs on its own plan ladder from $3/month, with a file manager and a database browser in the dashboard — which is where you check what a posts_per_page change actually did to query counts, without needing SFTP and a separate database client.
How this fits the rest of the stack
Set posts per page in Settings, Reading; use pre_get_posts when one archive needs a different number; and when page 2 404s, look for a custom WP_Query missing its paged parameter — then consider whether that query should exist at all. Keep paginated pages self-canonical and indexable, and always call wp_reset_postdata(). The RunxBuild hosting calculator shows managed WordPress plans with the dashboard tools for checking the result.
Useful related references:
- Squarespace vs WordPress: The Question Behind the Question
- WordPress Maintenance Services: What You Are Actually Paying For
- WP-CLI: The Commands That Make WordPress Administration Bearable
- Services on RunxBuild
FAQ
How do I change the number of posts per page in WordPress?
Settings, Reading, “Blog pages show at most”. That governs the main query on the blog index, archives, categories, tags and search. The RSS feed has a separate setting on the same screen.
Why does page 2 of my blog return a 404?
Almost always a custom WP_Query missing the paged parameter, combined with a main query using different settings. WordPress decides whether the page exists from the main query, so it 404s before your custom loop renders. Add paged, or use pre_get_posts instead.
How do I change posts per page for one archive only?
Hook pre_get_posts and call $query->set( 'posts_per_page', N ) inside a check for is_admin() and $query->is_main_query(). This modifies the main query rather than replacing it, so pagination stays consistent.
Should paginated pages be noindexed or canonicalised to page 1?
Neither, in general. Canonicalising page 2 to page 1 tells search engines they are duplicates and strips the internal links to posts listed only on later pages. Keep each page self-canonical and give the titles distinct page numbers.
Why do I need wp_reset_postdata after a custom loop?
Because the loop leaves the global post object pointing at its last post. Anything rendered afterwards — sidebar, footer, template tags — then reads that post instead of the correct one.