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:
- Bootstrap. Load configuration, connect to the database, load core.
- Load extensions. Load must-use plugins, plugins and the theme's
functions.php. - Work out what was requested. Turn the URL into query variables and run the main query.
- 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:
// index.php (core, simplified)
define( 'WP_USE_THEMES', true );
require __DIR__ . '/wp-blog-header.php';
From there WordPress loads, in order:
wp-blog-header.php, which loads WordPress and then runs the query and the template loader.wp-load.php, which definesABSPATHand findswp-config.php.wp-config.php, your configuration constants.wp-settings.php, the main bootstrap file. It loads the core files inwp-includes, connects to the database through the global$wpdbobject 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:
| Order | What happens | Hook that fires afterwards |
|---|---|---|
| 1 | Must-use plugins in wp-content/mu-plugins load | muplugins_loaded |
| 2 | Active plugins load, normally in alphabetical order of their paths | plugins_loaded |
| 3 | Pluggable functions such as wp_mail() are defined if no plugin replaced them | |
| 4 | The theme is about to load | setup_theme |
| 5 | The child theme's and parent theme's functions.php load | after_setup_theme |
| 6 | WordPress is fully loaded, the current user is set | init |
| 7 | Everything is ready, before the request is handled | wp_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 onafter_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:
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:
// 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>fireswp_head, where styles, scripts and meta tags are printed.- The Loop renders the posts from the main query.
wp_footer()before</body>fireswp_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:
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:
// 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.phploadswp-blog-header.php, which bootstraps WordPress throughwp-load.php,wp-config.phpandwp-settings.php.- Must-use plugins, plugins and then theme
functions.phpload beforeinit. - The URL is matched against rewrite rules, producing query variables for the main
WP_Query. template_redirectis 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.