Using switch_to_blog in WordPress Multisite to Query Across Network Sites

In a WordPress Multisite network each site has its own set of tables for posts, options, and term relationships, prefixed with its blog ID (e.g. wp_2_posts, wp_3_options). When plugin code, a cron job, or a network admin page needs to read data from a site other than the one currently being served, the standard API functions — get_posts(), get_option(), wp_get_nav_menus() — still work, but they target the current site’s tables by default. switch_to_blog() temporarily shifts all WordPress database queries and global state to a different site in the network. After your operations are done, restore_current_blog() switches back. This pattern is essential for network-level cron jobs that process all sites, network admin widgets that aggregate data, and any plugin feature that needs to read or write across sites without constructing raw SQL queries with explicit table prefixes. This article covers the correct usage pattern, common pitfalls, and a practical example that queries posts from all network sites.

Problem: You need to query posts, options, or other data from multiple sites in a WordPress Multisite network from a single PHP execution context — for example in a network cron job or a network admin dashboard widget.

Solution: Use switch_to_blog( $blog_id ) before running queries and restore_current_blog() afterwards. Always wrap in a try/finally block to guarantee restoration even on errors.

<?php
/**
 * Get the 5 most recent posts from every site in the network.
 * Returns an array keyed by blog_id.
 */
function get_network_recent_posts( $posts_per_site = 5 ) {
    if ( ! is_multisite() ) {
        return [];
    }

    $sites   = get_sites( [ 'number' => 100, 'public' => 1 ] );
    $results = [];

    foreach ( $sites as $site ) {
        $blog_id = (int) $site->blog_id;

        switch_to_blog( $blog_id );

        try {
            $posts = get_posts( [
                'post_type'      => 'post',
                'post_status'    => 'publish',
                'posts_per_page' => $posts_per_site,
                'orderby'        => 'date',
                'order'          => 'DESC',
                'no_found_rows'  => true,
            ] );

            $results[ $blog_id ] = array_map( function ( $post ) {
                return [
                    'id'        => $post->ID,
                    'title'     => get_the_title( $post->ID ),
                    'permalink' => get_permalink( $post->ID ),
                    'date'      => $post->post_date,
                    'site_name' => get_bloginfo( 'name' ),
                ];
            }, $posts );

        } finally {
            // Always restore, even if get_posts() throws
            restore_current_blog();
        }
    }

    return $results;
}

NOTE: switch_to_blog() is not lightweight — it clears WordPress's internal object cache and reloads site-specific globals. Avoid calling it in a loop over hundreds of sites on a front-end page request. Use it in background processing (WP-CLI, WP-Cron) or admin-only contexts. Also, switch_to_blog() does not switch the file system paths — get_template_directory() and ABSPATH still point to the original site. Only database context changes.