When working with WordPress, one of the most common tasks you’ll face as a developer or site owner is retrieving posts; whether you’re building a custom homepage, creating a widget, or listing a special category of posts, you’ll likely turn to one of three common methods: WP_Query, query_posts(), or get_posts().
All three are used to pull posts from the database, but each works in different ways; all have different levels of code flexibility, and each affects the main WordPress query differently. Knowing these differences lets you select the appropriate gadget for the work, write more effective code, and prevent a variety of unexpected issues with your site’s rendering.
In this article, we’ll dive deep into WordPress Query Functions ( WP_Query, query_posts(), and get_posts() ), explaining what they are, how they differ, and when to use each. Along the way, we’ll share code examples, best practices, and a Q&A section to clarify common questions.
By default, WordPress uses a “main query” to determine which posts to display on a given page. For example, if you visit your site’s homepage, WordPress runs a query behind the scenes to fetch the latest posts. If you visit a category archive, it queries posts from that category. If you view a single post, it queries that post by ID or slug.
Sometimes, though, you need more control. Perhaps you want to create a custom listing of posts from a specific category on your homepage, or you want to show only three posts with a certain tag in a sidebar widget. That’s when custom queries come into play, and WordPress gives you multiple tools to do this.
What is WP_Query?
WP_Query is a PHP class built into WordPress that lets you create custom queries to retrieve posts based on various parameters (like category, post type, author, or meta fields). It’s the most flexible and powerful method for running secondary queries in WordPress.

How It Works:
You instantiate a new WP_Query object and pass an array of arguments specifying what posts you want. For example:
$args = [
'post_type' => 'post',
'posts_per_page' => 5,
'category_name' => 'news'
];
$news_query = new WP_Query( $args );
if ( $news_query->have_posts() ) {
while ( $news_query->have_posts() ) {
$news_query->the_post();
// Display the post
the_title( '<h2>', '</h2>' );
the_excerpt();
}
wp_reset_postdata(); // Important after custom loops
}
Key Features of WP_Query:

What is query_posts()?
query_posts() is a function introduced early in WordPress history to modify the main query. By calling query_posts(), you effectively replace the main query’s parameters. It might seem convenient, but it has serious drawbacks.

How It Works:
query_posts() accepts an array of arguments similar to WP_Query:
query_posts( [
'category_name' => 'news',
'posts_per_page' => 5
] );
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
the_title( '<h2>', '</h2>' );
}
}
Why query_posts() is Problematic:

What is get_posts()?
get_posts() is a simpler function that returns an array of WP_Post objects. It’s basically a wrapper around WP_Query with a simplified set of defaults. It doesn’t set up the WordPress loop or the global $post variable; it just returns an array of posts that match your parameters.

How It Works:
Like WP_Query, you pass an array of arguments. For example:
$recent_posts = get_posts( [
'numberposts' => 5,
'category_name' => 'news'
] );
foreach ( $recent_posts as $post ) {
setup_postdata( $post );
the_title( '<h2>', '</h2>' );
the_excerpt();
}
wp_reset_postdata();
Key Features of get_posts():

Use WP_Query when:
Use get_posts() when:
Avoid query_posts():
If you must alter the main query, use the pre_get_posts action hook instead:
add_action( 'pre_get_posts', 'modify_main_query' );
function modify_main_query( $query ) {
if ( $query->is_main_query() && !is_admin() ) {
$query->set( 'posts_per_page', 5 );
}
}

Scenario 1: A Custom Homepage with a Hero Section and a News Section
You might use WP_Query for the hero section to display a single featured post and then get_posts() to fetch a few recent news posts in a sidebar quickly:
// Hero section with WP_Query
$hero_query = new WP_Query( [
'post_type' => 'post',
'posts_per_page' => 1,
'meta_key' => 'is_featured',
'meta_value' => 'yes'
] );
if ( $hero_query->have_posts() ) {
while ( $hero_query->have_posts() ) {
$hero_query->the_post();
the_title( '<h1>', '</h1>' );
the_post_thumbnail();
}
wp_reset_postdata();
}
// News section with get_posts()
$news_posts = get_posts( [
'numberposts' => 3,
'category_name' => 'news'
] );
echo '<div class="news-section">';
foreach ( $news_posts as $post ) {
setup_postdata( $post );
the_title( '<h2>', '</h2>' );
}
wp_reset_postdata();
echo '</div>';

Scenario 2: Modifying the Main Query for a Category Archive
Instead of using query_posts(), you use pre_get_posts:
add_action( 'pre_get_posts', 'modify_category_archive' );
function modify_category_archive( $query ) {
if ( $query->is_main_query() && $query->is_category( 'news' ) && !is_admin() ) {
$query->set( 'posts_per_page', 10 );
}
}
It ensures that the main query for the “news” category archive shows 10 posts without hijacking the query at the template level and without causing double queries.

To understand why a query returns unexpected results, consider these steps:
Don’t Modify the Main Query at Template Level with query_posts():
If you must alter the main query (for instance, to change the number of posts on the homepage), use pre_get_posts:
function homepage_posts( $query ) {
if ( $query->is_home() && $query->is_main_query() ) {
$query->set( 'posts_per_page', 5 );
}
}
add_action( 'pre_get_posts', 'homepage_posts' );

Deciding between WP_Query, query_posts(), and get_posts() is crucial for writing clean, efficient, and maintainable code in WordPress. Each approach has its purpose:
If you know these tools, you’ll design websites that perform better, behave as you expect, and are easier to maintain; this guide enables you to query posts in WordPress comfortably.

Hassan Tahir wrote this article, drawing on his experience to clarify WordPress concepts and enhance developer understanding. Through his work, he aims to help both beginners and professionals refine their skills and tackle WordPress projects with greater confidence.