20 Medium-Advanced WordPress Interview Q&As
1. Core Architecture & Execution Flow
Q1: Walk through the core WordPress load sequence. What happens from index.php to template rendering?
Answer:
index.phpdefinesWP_USE_THEMESand loadswp-blog-header.php.wp-blog-header.phpexecuteswp-load.php(which bootstraps the system viawp-config.phpandwp-settings.php) and callswp().wp-settings.phploads MUST-USE plugins, active plugins, themefunctions.php, and triggers theplugins_loaded,setup_theme, andinithooks.wp()calls$wp->main(), triggeringparse_requestandwp_selection_queryto populate the global$wp_queryobject.- Template Hierarchy:
template-loader.phpdetermines the correct theme template file based on$wp_queryflags (e.g.,is_single(),is_archive()) and renders the output.
Q2: What is the exact difference between init, wp_loaded, and wp hooks? When should you use each?
Answer:
init: Fires after WordPress is fully loaded, plugins are initialized, and user authentication is complete, but before headers or the main query are executed. Best for registering Custom Post Types (CPTs), taxonomies, and shortcodes.wp_loaded: Fires after the entire setup andinitprocess is finished. Ideal for executing actions that require everything in WordPress to be fully ready, such as processing early form submissions or custom redirects before any output.wp: Fires after the mainWP_Queryis established and conditional tags (is_single(),is_page()) are available, but before output begins. Use this when logic depends on the specific queried post/page context.
2. Hooks, Actions, and Filters
Q3: How does the $wp_filter global array structure callbacks, and what happens under the hood when apply_filters() is executed?
Answer: $wp_filter stores hook implementations as an array of WP_Hook objects indexed by tag name. Inside each WP_Hook object, callbacks are nested by priority level, then by a unique callback key generated via _wp_filter_build_unique_id().
When apply_filters('my_filter', $value) is called, WordPress retrieves the corresponding WP_Hook object, iterates through the priority levels in ascending numerical order, executes each callback function, and passes the return value of each function into the next callback in the chain.
Q4: What is the difference between remove_action() on a standard function versus an anonymous closure or OOP class method?
Answer:
- Standard Function: Passing the string function name directly to
remove_action('hook', 'function_name', $priority)works instantly. - Class Method: You must pass an identical array reference to the instance:
remove_action('hook', [$class_instance, 'method_name'], $priority). If the instance was instantiated anonymously or is out of scope, removing it requires iterating over the global$wp_filterarray to match the class name manually. - Anonymous Closures: Closures generate a randomized unique hash when added. Unless you store the closure reference in a variable or property at instantiation time, you cannot unhook it via
remove_action()without custom reflection scanning on$wp_filter.
3. Database Schema & Query Optimization
Q5: How does WP_Query construct SQL under the hood for complex meta_query blocks, and why can heavy meta queries cripple performance?
Answer: WP_Query parses meta_query parameters using the WP_Meta_Query class, generating SQL INNER JOIN or LEFT JOIN clauses against the wp_postmeta table for each array condition.
Because meta_value is stored as LONGTEXT (unindexed for deep text queries) and meta_key/meta_value relationships require non-clustered dynamic joins, running multiple nested AND/OR conditions requires MySQL to create temporary tables and perform filesorts across massive postmeta sets.
To optimize, offload filtering to custom taxonomy tables, store searchable fields in dedicated custom database tables, or use external search indices like Elasticsearch.
Q6: Explain the difference between wp_cache_get() (Object Cache) and Transient API functions. When would you use each?
Answer:
- Object Cache (
wp_cache_set/get): Non-persistent by default (lives only for the current HTTP request lifetime). If an external persistent cache drop-in (Redis/Memcached) is installed, object caching becomes persistent across requests across memory layers. - Transients (
set_transient/get_transient): Specifically designed to store temporary expired data in the database (wp_optionstable) as fallback. However, if an object cache drop-in is active,set_transient()automatically redirects to the object cache system in memory instead of writing towp_options.
Use Object Cache for low-latency, high-frequency data structures, and Transients for external API responses or heavy dataset calculations where fallback persistent database storage is required.
Q7: How do you safely execute custom database operations in WordPress using $wpdb to prevent SQL Injection and database corruption?
Answer:
$wpdb->prepare(): Always wrap queries containing dynamic input inside$wpdb->prepare(), using placeholders (%sfor strings,%dfor integers,%ffor floats). Never sanitize input before$wpdb->prepare()withesc_sql()manually if you are using placeholders.- CRUD Methods: Use built-in wrapper methods
$wpdb->insert(),$wpdb->update(), and$wpdb->delete(), which callprepare()under the hood. - Schema Updates: For custom tables, use
dbDelta()fromwp-admin/includes/upgrade.php, adhering strictly to SQL layout standards (two spaces afterPRIMARY KEY, explicit field lengths) to allow smooth migration upgrades.
4. Custom Post Types & Rewrite API
Q8: How does the WordPress Rewrite API translate a clean URL slug into internal query variables?
Answer:
- Registration:
add_rewrite_rule()registers a regular expression pattern mapped to internal query parameters (e.g.,'^catalog/([^/]+)/?' => 'index.php?product_category=$matches[1]'). - Matching: On request,
WP_Rewritetests the request URI against the array of rules stored in therewrite_rulesoption. - Query Variable Registration: Custom variables must be exposed via the
query_varsfilter, otherwiseWPstrips them out. - Parsing: Matching variables populate
$wp->query_vars, which are then passed into$wp_query->parse_query()to execute the main database loop.
Q9: What happens when you flush rewrite rules, and why should flush_rewrite_rules() never be called on init or unhooked page loads?
Answer: flush_rewrite_rules() re-populates the entire rewrite_rules array in the wp_options database table by regenerating rules from CPTs, taxonomies, and custom endpoints.
Calling it on every init triggers heavy CPU and DB operations (and writes to .htaccess/Nginx configurations if writing file rules), drastically degrading response times. It should only be executed inside activation/deactivation hooks (register_activation_hook) or when explicitly saved in admin settings screens.
5. REST API & Custom Endpoints
Q10: How do you register a custom, secure REST API route in WordPress with proper permission validation and schema sanitization?
Answer: Use register_rest_route() inside the rest_api_init hook, specifying namespace, route, and arguments:
PHP
add_action('rest_api_init', function () {
register_rest_route('myplugin/v1', '/data/(?P<id>d+)', [
'methods' => WP_REST_Server::READABLE, // 'GET'
'callback' => 'my_api_callback',
'permission_callback' => function ($request) {
return current_user_can('edit_posts'); // Never return true blindly
},
'args' => [
'id' => [
'validate_callback' => function($param) { return is_numeric($param); },
'sanitize_callback' => 'absint',
],
],
]);
});
permission_callback must be explicitly declared; omitting it or returning __return_true without intention opens security vectors.
Q11: How do REST API Nonces differ from standard WP Admin Nonces?
Answer: Standard WP Admin nonces validate CSRF protection on form submits via cookie verification. REST API nonces (passed via the X-WP-Nonce header or _wpnonce parameter) serve two distinct roles:
- They verify that the incoming REST API request originates from a user currently logged into an active browser session.
- If no valid nonce is supplied, the REST API sets the current user context to
0(Unauthenticated Guest/Logged-out), even if session cookies are present. This prevents unauthorized CSRF state-changing REST calls.
6. Security & Input Hygiene
Q12: What is the exact functional distinction between esc_attr(), esc_html(), esc_url(), and wp_kses()?
Answer:
esc_attr(): Encodes special characters (<,>,",',&) into HTML entities specifically safe for insertion into HTML element attributes (e.g.,value="...").esc_html(): Encodes special characters for HTML body context, converting raw markup so it displays as plain text rather than executing code.esc_url(): Cleans URLs by stripping invalid protocol schemes, escaping space entities, and rejecting dangerous inline scripts likejavascript:.wp_kses(): An advanced HTML sanitizer that strips all HTML tags and attributes except those explicitly defined in a whitelist array parameter (or presets likewp_kses_post()), making it ideal for rich text editor fields.
Q13: Explain how WordPress Nonces work under the hood. Are they true cryptographic nonces?
Answer: No. True cryptographic nonces are “number used once” keys. WordPress nonces are time-dependent HMAC hashes generated using:
- An action string (
$action). - The active user ID (
$user_id). - The current user session token.
- A time-based tick derived from Unix timestamps (
wp_nonce_tick()).
They remain valid for a specified time window (typically 12–24 hours, split into two ticks) and can be used multiple times within that window. They protect against CSRF (Cross-Site Request Forgery), not replay attacks.
7. Performance & High-Load Architecture
Q14: How does WordPress handle autoloaded options, and how do you diagnose and fix a bloated wp_options table?
Answer: When WordPress initializes, it runs a single query: SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'. If this data size exceeds a few megabytes, PHP memory spikes on every single page load.
Diagnosis & Remediation:
- Run a query to calculate total autoloaded payload size:SQL
SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload = 'yes'; - Identify the largest individual options:SQL
SELECT option_name, LENGTH(option_value) AS option_size FROM wp_options WHERE autoload = 'yes' ORDER BY option_size DESC LIMIT 20; - Convert non-critical plugin/transient configuration values to
autoload = 'no'usingadd_option()parameters or update queries, or clean up orphaned transients using WP-CLI (wp transient delete --expired).
Q15: What strategies would you implement to make a WooCommerce store with 100,000+ products run efficiently?
Answer:
- Enable High-Performance Order Storage (HPOS): Migrate orders from
wp_posts/wp_postmetato dedicated custom database tables (wp_wc_orders). - Offload Search and Filtering: Implement Elasticsearch or Algolia to bypass native
wp_postmetatax queries for catalog filter widgets. - Disable Unnecessary Cart Fragments: Disable or AJAX-throttle
wc-ajax=get_refreshed_fragmentson non-ecommerce landing pages. - Persistent Object Caching: Use Redis to cache queries, product objects, and transients.
- Exclude Session Cookies from Page Caching: Configure Varnish/Nginx edge cache rules to skip cache only when specific cookies (
woocommerce_items_in_cart,wp_woocommerce_session_) are detected.
8. Block Editor (Gutenberg) & Modern JS Integration
Q16: What is the difference between dynamic blocks and static blocks in Gutenberg, and when would you use each?
Answer:
- Static Blocks: The block’s markup structure is serialized directly into post content (
post_contentinwp_posts) via the Javascriptsave()function at edit time. Used for static layouts that rarely change structure globally. - Dynamic Blocks: The block saves raw attributes (or minimal markup) into
post_contentcomments, but rendering is handled server-side in PHP via arender_callbackfunction (orrender.phptemplate) on every request. Used when block content depends on real-time data (e.g., recent post lists, live stock counts, user context).
Q17: How does block.json improve block registration performance compared to legacy registerBlockType() JavaScript calls?
Answer: block.json provides a standardized metadata schema that allows WordPress to parse block properties server-side in PHP via register_block_type_from_metadata().
This enables:
- Asset Optimization: Enqueuing scripts/styles conditionally only when the block is present on the requested page (
script,style,editorScript). - SSR Optimization: Registering metadata once on the backend so both PHP and JS share structural contexts without duplicate definitions.
- Lazy Loading: Gutenberg can defer loading block assets until the user interacts with or scrolls to the block in the editor canvas.
9. Object-Oriented PHP & Plugin Architecture
Q18: What is the recommended way to handle dependency injection and singleton patterns in modern WordPress plugin architecture?
Answer: While many classic plugins use simple Singleton classes (getInstance()) to maintain global state, modern standards prefer Dependency Injection (DI) combined with a lightweight PSR-11 compliant Service Container.
- Services (like Database API wrappers, REST controllers, and Payment Gateways) are injected into class constructors rather than instantiated via global variables or singletons.
- This decouples business logic from WordPress core functions, allowing unit testing via PHPUnit and Brain Monkey where core dependencies can be mocked cleanly.
Q19: How do Action Scheduler and wp_cron differ in execution reliability for heavy background processing?
Answer:
wp_cron: Triggered strictly by page visits. If a site receives low traffic, scheduled tasks miss execution times. Long-running cron tasks can also time out or spawn concurrent race conditions if an HTTP request spawns multiple overlapping cron executions.- Action Scheduler: A background processing library (used by WooCommerce) that stores tasks as custom post types or dedicated tables (
wp_actionscheduler_actions). It breaks heavy operations into small queues, executes via background batching, tracks success/failure states, and provides retry mechanisms, preventing memory crashes during large processing jobs.
10. Multisite Architecture
Q20: How does table prefixing work in a WordPress Multisite network, and how do you execute cross-site queries cleanly?
Answer: In WordPress Multisite:
- Network-wide shared tables use the base prefix:
wp_users,wp_usermeta,wp_blogs,wp_site. - Subsite-specific tables append the site ID:
wp_2_posts,wp_2_options,wp_3_posts, etc. (Site 1 uses the basewp_posts).
To run cross-site operations, never manually construct table names with raw prefix strings. Use switch_to_blog($blog_id) and restore_current_blog():
PHP
switch_to_blog(2);
// Executes against wp_2_posts and site 2 context
$posts = get_posts(['post_type' => 'post']);
restore_current_blog(); // Always restore to avoid corrupting active site context