Multisite turns one WordPress installation into a network of sites that share the same code, users table and database. It is powerful for the right job and a burden for the wrong one. As a plugin developer you need to understand it even if you never run a network, because your code may be installed on one.
When Multisite fits
All sites in a network share core, plugins and themes. A plugin is installed once and either activated per site or network-activated for all. Users exist once and can be members of several sites.
Good fits:
- Many similar sites run by one organisation: university departments, franchise locations, regional or language sites.
- A platform where users create their own sites from a fixed set of themes and plugins.
- Sites that genuinely need shared users and central updates.
Poor fits:
- Unrelated client sites on one server. One faulty plugin update or one bad query affects every site, and you cannot move a single site to another host as easily.
- Sites that need very different plugins or different PHP versions.
- Sites with different owners who each need full administrator control. On a network, only super admins can install plugins and themes.
Multisite is not the only way to manage many sites. Separate installs managed with WP-CLI and a shared deployment pipeline are often simpler and isolate failures better.
How a network is organised
| Term | Meaning |
|---|---|
| Network | The whole installation. Code calls it a "site" in older APIs, such as get_site_option(). |
| Site | One website in the network. Older APIs call it a "blog", as in switch_to_blog(). |
| Super admin | A user who manages the network: sites, plugins, themes, network settings. |
| Network admin | The admin area at /wp-admin/network/. |
Subdirectories or subdomains
You choose once, at setup:
- Subdirectories:
example.com/shop,example.com/news. - Subdomains:
shop.example.com,news.example.com. This needs wildcard DNS and a certificate that covers the subdomains.
Individual sites can also use their own custom domain, set in the site's URL in the network admin, provided DNS and certificates point to the server.
The database
Global tables are shared: wp_users, wp_usermeta, wp_site, wp_sitemeta, wp_blogs and a few others. The first site uses the normal tables (wp_posts, wp_options). Each new site gets its own set with its ID in the name: wp_2_posts, wp_2_options, and so on. Uploads for site 2 go to wp-content/uploads/sites/2/.
This is why code must use $wpdb->posts and $wpdb->prefix, never hard-coded table names. $wpdb->prefix changes to wp_2_ when site 2 is active, while $wpdb->base_prefix stays wp_.
Setting up a network
Start from a working single site, with a backup.
- Allow Multisite in
wp-config.php, above the "That's all, stop editing!" line:
define( 'WP_ALLOW_MULTISITE', true );
- Deactivate all plugins, then go to Tools > Network Setup, choose subdirectories or subdomains and install.
- WordPress shows the constants to add to
wp-config.phpand the rewrite rules for.htaccessor your Nginx configuration. They look like this:
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', false );
define( 'DOMAIN_CURRENT_SITE', 'example.com' );
define( 'PATH_CURRENT_SITE', '/' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );
- Log in again and you will find My Sites > Network Admin in the admin bar.
WP-CLI can do the conversion and manage sites:
wp core multisite-convert
wp site create --slug=news --title="News"
wp site list --fields=blog_id,url
wp plugin activate myplugin --network
wp super-admin list
Most WP-CLI commands act on the main site unless you pass --url:
wp --url=example.com/news option get blogname
wp site list --field=url | xargs -I {} wp --url={} cache flush
Writing Multisite-aware code
Detecting the environment
if ( is_multisite() ) {
$site_id = get_current_blog_id();
}
Site options versus network options
get_option() and update_option() read and write the current site's options table. For settings that apply to the whole network, use network options, stored in wp_sitemeta:
$api_key = get_site_option( 'myplugin_api_key', '' );
update_site_option( 'myplugin_api_key', $new_key );
On a single site these fall back to normal options, so the same code works everywhere. Decide deliberately which settings are per site and which are per network.
Capabilities
On a network, super admins pass every capability check on every site. Some capabilities, such as manage_network_options, manage_network_plugins and manage_sites, only make sense for super admins. Check capabilities as usual; do not test roles. If a screen belongs in the network admin, register it on the network_admin_menu action instead of admin_menu.
Switching sites
switch_to_blog() changes the current site for the database and options, so you can read or write another site's content. Every call must be paired with restore_current_blog().
/**
* Collect the latest post title from every public site in the network.
*/
function myplugin_latest_titles(): array {
$titles = array();
$site_ids = get_sites(
array(
'fields' => 'ids',
'public' => 1,
'archived' => 0,
'deleted' => 0,
'number' => 50,
)
);
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
$posts = get_posts(
array(
'numberposts' => 1,
'no_found_rows' => true,
)
);
if ( $posts ) {
$titles[ $site_id ] = get_the_title( $posts[0] );
}
restore_current_blog();
}
return $titles;
}
What switching does not do matters as much:
- It does not load the other site's plugins or theme. If a custom post type is registered only on site 5, it is not registered while you are switched to site 5 from site 1.
get_posts()still works on its rows, but templates, filters and settings from that site's plugins are absent. - It is not free. Each switch changes cache context. Looping over hundreds of sites in a page request is slow; cache the combined result, or build it in a scheduled job.
- Nesting needs matching restores. Two switches need two restores. A missed restore leaves the rest of the request on the wrong site, which is a confusing bug.
For network-wide data you will read often, consider maintaining it in one place (a network option or a table using $wpdb->base_prefix) instead of switching on every request.
Network activation
A plugin can be activated on one site or for the whole network. The activation hook receives a flag telling you which:
register_activation_hook( __FILE__, 'myplugin_activate' );
function myplugin_activate( bool $network_wide ): void {
if ( is_multisite() && $network_wide ) {
// Store network-level defaults only. Per-site setup runs lazily.
add_site_option( 'myplugin_network_version', '1.0.0' );
return;
}
myplugin_setup_site();
}
function myplugin_setup_site(): void {
add_option( 'myplugin_settings', array( 'enabled' => true ) );
}
Avoid looping over every site in the activation hook. On a large network it can time out halfway. Instead, check per site when it is first used (for example on admin_init, comparing a stored version) and run setup then, or queue the work with WP-Cron.
To force network activation only, add Network: true to the plugin header.
New sites
When a site is created, the wp_initialize_site action fires with the new WP_Site object. Core uses this hook at priority 10 to create the site's tables, so hook in later:
add_action( 'wp_initialize_site', 'myplugin_new_site', 20, 1 );
function myplugin_new_site( WP_Site $site ): void {
// This code lives in the main plugin file, so __FILE__ is the plugin file.
if ( ! is_plugin_active_for_network( plugin_basename( __FILE__ ) ) ) {
return;
}
switch_to_blog( (int) $site->blog_id );
myplugin_setup_site();
restore_current_blog();
}
is_plugin_active_for_network() is defined in wp-admin/includes/plugin.php. It is available during site creation in the admin; if you call it elsewhere, require_once ABSPATH . 'wp-admin/includes/plugin.php'; first.
Uninstalling cleanly
uninstall.php runs once, in the context of the main site. To remove per-site data from every site, loop through sites with switch_to_blog() there, and delete network options with delete_site_option().
Summary
- Multisite shares code, users and one database across many sites; it suits related sites run centrally, not unrelated client sites.
- Enable it with
WP_ALLOW_MULTISITE, run Network Setup, then add the generated constants and rewrite rules. - Each site has its own tables (
wp_2_posts); always use$wpdb->prefixand core table properties. - Use
get_site_option()for network settings andget_option()for per-site settings. - Pair every
switch_to_blog()withrestore_current_blog(), remember it does not load the other site's plugins, and avoid switching across many sites per request. - Handle network activation and
wp_initialize_sitewithout looping over every site at once.