Introduction
Type a URL, press Enter, and a WordPress page appears. Between those two moments WordPress runs a precise sequence of steps: it boots itself up, loads your plugins and theme, works out what content you asked for, fetches it from the database, and renders it as HTML. That sequence is the request lifecycle.
Understanding it is one of the highest-leverage things you can learn as a WordPress developer. Almost every “why isn’t my code running?” problem comes down to hooking in at the wrong point in this sequence.
You will learn:
- The six phases every WordPress request goes through
- How WordPress bootstraps itself from index.php
- When your plugins and theme load, and the hooks that fire
- How WordPress decides what content to show and which template to use
- Where to hook your own code for common tasks
The Big Picture: Six Phases of a Request
- Entry point — the web server hands the request to a single PHP file.
- Bootstrap — WordPress loads its configuration and core code.
- Plugins and theme — your plugins and the active theme load, and setup hooks fire.
- Request parsing and the main query — WordPress works out what was asked for and queries the database.
- Template and the Loop — WordPress picks a template file and renders the content.
- Output and shutdown — the finished HTML is sent and final hooks fire.
Phase 1: The Web Server and the Entry Point
Every front-end request, whatever the URL, is routed to a single file: index.php. On Apache, the .htaccess file rewrites pretty URLs into a request for index.php. On Nginx, equivalent rules do the same job.
The root index.php is deliberately tiny:
<?php
define( 'WP_USE_THEMES', true );
require __DIR__ . '/wp-blog-header.php';
WP_USE_THEMES tells WordPress to render a theme. Then it hands off to wp-blog-header.php, which orchestrates the rest.
Phase 2: Bootstrapping WordPress
wp-blog-header.php runs three steps in order:
require_once __DIR__ . '/wp-load.php'; // load WordPress
wp(); // run the main query
require_once ABSPATH . WPINC . '/template-loader.php'; // render the template
The bootstrap chain:
- wp-load.php finds wp-config.php — in the WordPress root first, then one directory above.
- wp-config.php defines your settings. Its final line hands control to core: require_once ABSPATH . ‘wp-settings.php’.
- wp-settings.php loads core function files, sets up the database ($wpdb), loads must-use plugins, loads active plugins, loads the active theme, and fires setup hooks throughout.
Phase 3: Plugins, the Theme, and the Setup Hooks
The key setup hooks fire in this fixed order:
- muplugins_loaded — after must-use plugins load.
- plugins_loaded — after all active plugins are loaded. Use for early setup that depends on other plugins being available.
- after_setup_theme — around when the theme’s functions.php loads. Register theme support features here.
- init — after WordPress is fully loaded, before any request handling. The workhorse hook for registering post types, taxonomies, shortcodes, and most setup.
- wp_loaded — the last setup hook, after everything is loaded but before the request is parsed.
// Correct: init is the standard place to register content types.
add_action( 'init', 'kubic_register_book_type' );
function kubic_register_book_type() {
register_post_type( 'book', array( 'public' => true ) );
}
If you call a function before the file that defines it has loaded, you get a “call to undefined function” error. Moving the code to init fixes it almost every time.
Phase 4: Parsing the Request and Running the Main Query
wp() parses the URL into query variables, then builds a WP_Query and asks the database for the matching posts. You can modify what the main query fetches with pre_get_posts, which fires just before the query runs:
add_action( 'pre_get_posts', 'kubic_posts_per_page' );
function kubic_posts_per_page( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
$query->set( 'posts_per_page', 12 );
}
}
The is_main_query() check matters. pre_get_posts runs for every query on the page — confirm you are altering the main query and not something else. This is the correct, performant way to change what the page shows. Building a brand-new WP_Query for the main content is a common and costly mistake.
Phase 5: Choosing a Template and Running the Loop
template-loader.php first fires template_redirect — the last chance to intercept a request or redirect the user before any HTML is produced.
Then WordPress chooses a template file using the template hierarchy: a predictable set of rules mapping the request type to a file in your theme, falling back to index.php. You can override the final choice with template_include:
add_filter( 'template_include', 'kubic_landing_template' );
function kubic_landing_template( $template ) {
if ( is_page( 'launch' ) ) {
return plugin_dir_path( __FILE__ ) . 'templates/launch.php';
}
return $template;
}
Inside the template, wp_head() fires the wp_head action — this is where wp_enqueue_scripts runs and styles and scripts registered on that hook are output. wp_footer() fires near the closing body tag, and deferred scripts are output there.
Phase 6: Output and Shutdown
The assembled HTML is sent to the browser. PHP then fires the shutdown action — the final hook of the request. WordPress does not stay running between requests; it boots, responds, and exits every single time.
The Lifecycle Hooks in Order
- muplugins_loaded — must-use plugins loaded
- plugins_loaded — all active plugins loaded
- after_setup_theme — theme functions.php loaded
- init — WordPress fully loaded; register content here
- wp_loaded — end of the load phase
- parse_request — URL parsed into query vars
- send_headers — HTTP headers sent
- pre_get_posts — just before the main query runs
- wp — request environment fully set up
- template_redirect — last chance before output
- template_include — final template file chosen
- wp_enqueue_scripts — enqueue scripts and styles (fires during wp_head)
- wp_head / wp_footer — output injected into the page
- shutdown — request finished
Why This Matters in Practice
- “My function says undefined.” — You called it before its file loaded. Hook later, usually on init.
- “My post type doesn’t work.” — Register it on init.
- “My query changes broke pagination.” — Use pre_get_posts instead of a new WP_Query.
- “My script won’t enqueue.” — Add it inside the wp_enqueue_scripts hook.
Best Practices
- Register content types and taxonomies on init.
- Declare theme support on after_setup_theme.
- Filter the main query with pre_get_posts, never replace it.
- Enqueue scripts and styles on wp_enqueue_scripts.
- Never edit core bootstrap files — use hooks instead.
Common Mistakes
Hooking too early. Attaching code to plugins_loaded when it needs WordPress fully loaded leads to undefined functions. When in doubt, use init.
Replacing the main query. Using query_posts() or creating a new WP_Query for the page’s primary content breaks pagination and runs an extra query. Use pre_get_posts.
Outputting before headers are sent. Echoing text or leaving whitespace before WordPress sends its headers causes “headers already sent” errors.
Doing heavy work on every request. Code on init runs on every request. Cache results, or run expensive work only when the specific condition applies.
FAQ
Which hook should I use to run code on every request?
Use init for almost everything. Reach for plugins_loaded only when you genuinely need to run before init.
What is the difference between plugins_loaded and init?
plugins_loaded fires as soon as all plugins are in memory, before the theme and the rest of setup. init fires later, once WordPress is fully loaded. Use plugins_loaded for early cross-plugin coordination and init for normal setup.
How does WordPress decide which template file to load?
After template_redirect, it walks the template hierarchy, a fixed set of rules matching the request type to a file in your theme, falling back to index.php. Override the final file with the template_include filter.
Do my hooks fire on AJAX and REST API requests?
Partially. Both bootstrap WordPress — plugins_loaded and init fire normally. However, they skip the template phase entirely. wp() does not run, so there is no main query, and wp_head/wp_footer never fire. Register REST routes and AJAX handlers on init, and keep them independent of front-end template logic.
Does WordPress stay running between requests?
No. WordPress boots from scratch, handles the request, and exits every time. State is stored in the database or a cache rather than in memory between requests.
Conclusion
A WordPress request is an orderly sequence: the server hands off to index.php, WordPress bootstraps through wp-load and wp-settings, plugins and the theme load while setup hooks fire, the URL is parsed and the main query runs, a template renders the content, and the page is sent before WordPress shuts down.
Match the task to the right hook — init for setup, pre_get_posts for the main query, template_include for templates, wp_enqueue_scripts for assets — and most timing problems disappear.