How WordPress loads a page

Follow a request from index.php through plugins, the theme, the main query and the template, and see where each hook fires.

  • Intermediate
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Trace a request through WordPress
  • Know when hooks fire
  • Explain how a URL becomes a query and a template

Before you start

Most WordPress bugs that confuse developers are timing bugs: code that runs too early, before the thing it needs exists, or too late, after the page has been sent. Once you know the order in which WordPress loads, you can pick the right moment for every piece of code.

This lesson follows one front-end request, such as https://example.com/2026/10/hello-world/, from start to finish.

The big picture

Every front-end request goes through four stages:

  1. Bootstrap. Load configuration, connect to the database, load core.
  2. Load extensions. Load must-use plugins, plugins and the theme's functions.php.
  3. Work out what was requested. Turn the URL into query variables and run the main query.
  4. Render. Choose a template from the theme and output the page.

Hooks fire between and inside each stage. Your code runs when one of those hooks fires.

Stage 1: bootstrap

The web server's rewrite rules send every request that is not a real file to index.php in the site root. That file is tiny:

PHP
// index.php (core, simplified)
define( 'WP_USE_THEMES', true );
require __DIR__ . '/wp-blog-header.php';

From there WordPress loads, in order:

  1. wp-blog-header.php, which loads WordPress and then runs the query and the template loader.
  2. wp-load.php, which defines ABSPATH and finds wp-config.php.
  3. wp-config.php, your configuration constants.
  4. wp-settings.php, the main bootstrap file. It loads the core files in wp-includes, connects to the database through the global $wpdb object and sets up defaults.

Nothing of yours has run yet, apart from wp-config.php.

Stage 2: plugins and theme

Still inside wp-settings.php, WordPress loads extensions. The important hooks fire here:

OrderWhat happensHook that fires afterwards
1Must-use plugins in wp-content/mu-plugins loadmuplugins_loaded
2Active plugins load, normally in alphabetical order of their pathsplugins_loaded
3Pluggable functions such as wp_mail() are defined if no plugin replaced them
4The theme is about to loadsetup_theme
5The child theme's and parent theme's functions.php loadafter_setup_theme
6WordPress is fully loaded, the current user is setinit
7Everything is ready, before the request is handledwp_loaded

This order explains several rules you will meet constantly:

  • A plugin file runs before the theme. If a plugin calls a function that the theme defines, it fails. Use hooks instead of calling code at the top of a file.
  • Register post types, taxonomies and shortcodes on init. That is the documented moment, and by then translations and the current user are available.
  • Theme features such as add_theme_support( 'post-thumbnails' ) go on after_setup_theme.

When a plugin file loads, almost nothing it needs exists yet. That is why plugin code mostly defines functions and attaches them to hooks, rather than doing work immediately.

Stage 3: from URL to query

Back in wp-blog-header.php, WordPress calls wp(). This runs the main request handler, the WP class.

Parsing the request

WordPress compares the URL path with its rewrite rules: a list of regular expressions stored in the options table. A match produces query variables. For /2026/10/hello-world/, the result is roughly:

PHP
array(
    'year'     => '2026',
    'monthnum' => '10',
    'name'     => 'hello-world',
)

The parse_request action fires after this step. REST API requests are detected here too: a URL under /wp-json/ is handed to the REST server, which fires rest_api_init, returns JSON and exits without loading a template.

Running the main query

The query variables are passed to the global $wp_query, an instance of WP_Query. This is the main query. Before it runs, WordPress fires pre_get_posts, which lets you change the query, for example to show more posts on an archive. Then it builds SQL, fetches the matching posts and sets flags such as is_single(), is_archive() and is_404().

After the query, the wp action fires. At this point you know what the visitor asked for.

Stage 4: choosing a template and rendering

Next comes template_redirect. This is the right place to redirect visitors, because nothing has been output yet but the query is known:

PHP
// wp-content/plugins/myplugin/myplugin.php
add_action( 'template_redirect', 'myplugin_redirect_old_page' );

function myplugin_redirect_old_page() {
    if ( is_page( 'old-pricing' ) ) {
        wp_safe_redirect( home_url( '/pricing/' ), 301 );
        exit;
    }
}

Then WordPress loads wp-includes/template-loader.php. It uses the template hierarchy to choose a file from the theme. For a single post it looks, in order, for single-post-hello-world.php, single-post.php, single.php, singular.php and finally index.php. The first file that exists wins. The template_include filter lets code swap the chosen file.

Block themes follow the same hierarchy but look for HTML templates in a templates folder, such as templates/single.html.

The template then outputs the page. Along the way it calls functions that fire hooks:

  • wp_head() in the <head> fires wp_head, where styles, scripts and meta tags are printed.
  • The Loop renders the posts from the main query.
  • wp_footer() before </body> fires wp_footer, where footer scripts are printed.

Finally, when PHP finishes, the shutdown action fires.

The whole sequence

The front-end hooks you will use most, in the order they fire:

Plain text
muplugins_loaded
plugins_loaded
setup_theme
after_setup_theme
init
wp_loaded
parse_request
pre_get_posts
wp
template_redirect
template_include   (filter)
wp_head
wp_footer
shutdown

Admin requests follow the same bootstrap and then go to wp-admin instead of the template loader. There admin_init fires early, and admin_menu fires when menus are built. AJAX and REST requests also share the bootstrap.

See it for yourself

You can print the order hooks fire on your own site. Add this temporarily to your plugin:

PHP
// wp-content/plugins/myplugin/myplugin.php, temporarily
$myplugin_hooks = array( 'plugins_loaded', 'after_setup_theme', 'init', 'wp_loaded', 'pre_get_posts', 'wp', 'template_redirect', 'wp_head', 'wp_footer' );

foreach ( $myplugin_hooks as $myplugin_hook ) {
    add_action(
        $myplugin_hook,
        static function () use ( $myplugin_hook ) {
            error_log( 'Fired: ' . $myplugin_hook );
        }
    );
}

Load a front-end page and read wp-content/debug.log. You will see each hook once, in order. pre_get_posts may appear more than once, because it fires for every query, not only the main one. Remove the code when you are done.

The free Query Monitor plugin shows every hook that fired on a page and the functions attached to it. It is the quickest way to answer "has this hook fired yet?".

Summary

  • index.php loads wp-blog-header.php, which bootstraps WordPress through wp-load.php, wp-config.php and wp-settings.php.
  • Must-use plugins, plugins and then theme functions.php load before init.
  • The URL is matched against rewrite rules, producing query variables for the main WP_Query.
  • template_redirect is the moment to redirect; the template hierarchy then picks a template.
  • Code runs at the right time by attaching to the right hook.

In the next lesson you will decide whether a piece of code belongs in a theme or a plugin.

Further reading