Libraries (plugins/lib)

Much as our Shared Plugins directory provides a wide variety of useful, battle hardened, and maintained plugins, Shared Libraries aims to provide a central repository of reusable and maintained code libraries for use by our VIP customers. Shared Libraries reduces the hassle of including, maintaining, and reviewing, and lets you focus on what’s really important: making amazing WordPress websites!

To get started with Shared Libraries, checkout plugins from SVN. All shared libraries live under plugins/lib.

To utilize a shared library in your code, simply include it with:

wpcom_vip_require_lib( 'codebird' );

We have various libraries such as OAuth (via OAuth.php), the Brightcove Media API, and WP_Codebird, a PHP Twitter client based on Codebird, with many more to come!

If you know of a solid, useful library that would be a good fit for Shared Libraries, let us know!

Plugins on VIP

Here at VIP, we maintain a list of plugins that have been reviewed and approved by our engineers.These pre-reviewed plugins help speed up review for your projects on VIP and ensure the 3rd-party code you’re using is safe and scalable.

There are 3 different “types” of plugins on VIP:

  • The Shared Plugin repository, which is reviewed and maintained by the VIP team
  • The Reviewed Plugins list, which are reviewed and recommended by the VIP team. These plugins are not maintained by the VIP team.
  • Custom Plugins developed by your team and added to your theme.

Shared plugins

The Shared Plugin repository is collection of plugins that are reviewed and maintained by the VIP team. As a VIP developer you will have read access to our shared plugins repository, which contains some Automattic plugins as well as plugins from our Featured Partners. There are two ways to activate a shared plugin: from your VIP Dashboard or in your theme’s code. We recommend choosing one method and sticking to it to make it easy to determine which plugins are enabled for your site.

Note: On VIP Go, shared plugins are not necessarily available automatically – see the mu-plugins repo for what’s available out of the box.

Activating a Shared Plugins from your Dashboard

Simply navigate to Plugins and Services in your VIP dashboard, find the plugin you’d like, and click “Activate.”

Activating a Plugin

Activating a Shared Plugin In Your Theme

Once you have SVN access to your VIP theme, you will also have read access to the Shared Plugins repository. To test these plugins in your development environment, use your username and password to check out a copy of them from using Subversion. To match our environment, they should be checked out to wp-content/themes/vip/plugins/ and not wp-content/plugins/ like you would normally.

svn co

To load plugins from the shared plugins folder, you can use the following function call:

wpcom_vip_load_plugin( 'zoninator', 'plugins', '0.6' );

Note that the third parameter, `$version` specifies the specific version you wish to load.

Updating Shared Plugins

We maintain two versions of each shared plugin at any one time, allowing you to upgrade to new versions at more of your own pace. We very much recommend you always use the latest available version which you can find by looking in the shared plugins repository itself.

New versions are announced through the VIP Lobby and the oldest version is deprecated simultaneously. Older versions are removed one month after a new version is committed, giving you plenty of time to test and upgrade.

There are two ways to go about updating your plugin, depending on how the plugin was activated (via dashboard or theme):

  • If the plugin was activated by dashboard, you will need to deactivate it on the dashboard and activate the newer version with code using the wpcom_vip_load_plugin helper function. For example, if we want to update Co-Authors-Plus to the latest version, we would stick the below in our functions.php:
    wpcom_vip_load_plugin( 'co-authors-plus', 'plugins', '3.2' );
  • If you’ve already activated your plugin by code, we would need to add or update the third parameter in the wpcom_vip_load_plugin helper function that you used to activate your plugin. For example, if we wanted to update an old version of Co-Authors-Plus to a new version, we would change the line from:
    wpcom_vip_load_plugin( 'co-authors-plus', 'plugins' );


    wpcom_vip_load_plugin( 'co-authors-plus', 'plugins', '3.2' );

If you need assistance, or would like to request an upgrade, don’t hesitate to open up a ticket with us.

Reviewed Plugins

Each time we review a plugin (or a new version), we update this list of Reviewed Plugins. You can bundle any VIP Reviewed Plugins into your theme, and they won’t need to be reviewed. To use a plugin on this list, simply commit the plugin to your theme in a single commit, letting us know in the message that it’s a reviewed plugin, and we’ll get it deployed. Please note that we do not maintain these plugins, so your team will be responsible for updates.

If an open-source plugin you’d like to use is not on this list, please open a ticket to ask us to review (more details on that here).

Activating plugins that you bundle in your theme is as simple as placing them into a plugins directory, and loading them within your functions.php like so;

wpcom_vip_load_plugin( 'custom-plugin', 'my_theme_directory_name' );


Custom Plugins

These are any plugins that you develop yourself and bundle in your theme. These will always be reviewed according to the same process as your theme – you can read more about plugin review here.

If the plugin is around 1,000 lines of code (excluding CSS), you can commit it to the deploy queue as part of your regular work. If it is larger, please zip the plugin and submit it via tickets for review.

Once your plugin is approved, add it to the “plugins” directory within your theme directory. To activate it, use wpcom_vip_load_plugin in functions.php with ‘my_theme_directory_name’ as the second parameter, e.g.:

wpcom_vip_load_plugin( 'custom-plugin', 'my_theme_directory_name' );

Cloud Hosting FAQ

Frequently-asked questions about our VIP Cloud Hosting. Have a question that’s not answered here? Get in touch. 

1) We don’t yet have a WordPress theme – how should we get started with VIP Hosting?

If you’re building a theme from scratch, we suggest using the WordPress default themes, Twenty Ten, Twenty Eleven, or Twenty Twelve, as they use the recent APIs, are cleanly coded, and provide a good foundation for your work. You should start your theme as a Child Theme instead of modifying Twenty Ten/Twenty Eleven/Twenty Twelve directly. All public themes are available at

2) We already have a theme on a self-hosted WordPress site. Will we be able to use it on VIP Hosting?

Yes, probably! As part of the setup process the VIP team will review your theme for any errors or code that needs to change to make sure it’s a good fit for our environment. We also require the use of GPL-licensed plugins and themes or 100% custom ones. If you’re not sure about what you’re using, just ask!

3) How long does the theme review process take?

Once you have your theme fully built and tested in your local environment, the theme is ready for us to review. We generally need 10-15 business days to do a full theme review. You’ll send it to us in an email as a zipped attachment.

4) Can we use X plugin?

Yes, probably! There are +23k WordPress plugins, and you can use most.  We review each plugin to be sure it’s enterprise quality, secure, and will scale with your site’s traffic.  Legacy methods of modifying database tables and manipulating the file system are also traits of plugins we try to avoid, and will work with you to use modern approaches such as Custom Post Types and in-memory file manipulations. We also have a bunch of popular plugins already pre-installed and ready for use. If you are interested in using a plugin that isn’t available as a pre-approved VIP plugin, we will review it for stability, performance, and security before it can be used on VIP.

We also recommend using existing WordPress functionality whenever possible instead of a plugin. If you are not sure if a function exists, try a quick search of a descriptive keyword/field name or similar on one of the many WordPress code documentation sites such as,, or

5) Can we use X caching plugin?

You won’t need any caching plugins on VIP Hosting. employs multiple levels of caching automatically. For cases when you are sure that you cannot use a function that contains caching and need to do something that is resource intensive, you should implement some caching yourself using the WordPress cache.

6) How will we access our code?

All code is managed using the Subversion (SVN) revision control system. SVN is the only way to access and update code on VIP Hosting. After your theme has been reviewed, you will be able to use your username and password to access the code for your theme using Subversion. The VIP team will review and handle all deploys for your site, and your developers will get deploy notifications automatically.

7) How does staging work?

VIP clients for the most part have staging servers in-house. A few clients choose to use one of their VIP-package sites as a pre-production site, but any code must be tested and debugged prior and aligned with our coding standards. Think of this pre-production site as a way to test integrations with features that don’t have equivalents (via Jetpack or other plugins).

8) Can we have custom database tables on

Themes and plugins should use the existing database tables and structure (no alterations). Custom Post Types, Custom Taxonomies, post meta, etc. are incredibly flexible as an alternative to custom database tables.

9) Can we use plugins or code that modify the filesystem?

For security and performance reasons we do not allow plugins or code that write directly to the filesystem.  We’ll work with you to modify the code to avoid local file system manipulations.

10) How do we register user accounts?

By default, users registered through VIP-hosted websites are created as users, which means that the millions of users logged in to will also be logged in on your domain, making it easier for them to comment on your site. If you’d like to be able to query user data or create custom registration fields you’ll need to use a 3rd-party registration service.

11) Is VIP running stock WordPress, or are there tons of custom modifications? is running mainly off core WordPress, but uses some alterations and optimizations which are unique to our large-scale environment, including a lot of very cool features built right in for all users. is constantly evolving, which means that VIP clients often have access to the latest features and WordPress releases well before the rest of the world. Easy-to-use shortcodes and other built-in features replace the need for many custom plugins.

Have a question that’s not answered here? Get in touch. 

Suggestions to Fast Track your Theme Review

1) Make sure you’ve reviewed VIP documentation

This site features documentation and a guide for developing sites on VIP. We share best practices, helpful tips and share information on custom modifications other VIP clients have implemented to help you code better, faster, and stronger. We highly recommend going through the Getting Started and Best Practices sections. In particular, make sure you’ve reviewed the Anatomy of a VIP Theme.

2) Make sure your local environment is set up correctly as described here:

3) Work with an existing theme that’s available on

If you’re looking for a barebones theme, to start from, check out _s, which is built and maintained our own Theme Team.

If you’re looking for something more fleshed out to work from, we recommend going through one of our 200+ pre-approved themes on The code for all the free themes are available here.

If you’re interested in working with a Premium theme, let us know and we can provide access to the code. When starting with a premium theme, we strongly recommend that you create a child theme based upon it and submit that. Modifying the code of the premium theme, itself, is something you simply shouldn’t do. Not to mention, the child theme approach will get you to the finish line a lot faster.

If you’re running a number of sites you’ll want to take the common theme approach – it’s a single theme used across multiple sites

If each site contains some unique functionality you can add that via Child Themes. Here’s an excellent walk-through.

4) Re-use and Recycle

Instead of replicating functionality by coding it anew, use some of the many existing plugins we have pre-approved, including some from our partners.

5) Follow Standards

We recommend all developers follow the WordPress Coding Standards and observe our Anatomy of a VIP Theme. It helps ensure consistency and cleanliness in code and helps us review things much faster. The Underscores project will generate a starter theme that follows best practices for you to build on.

6) Avoid Common Issues

Please review the list of common issues we see in Theme Reviews.

Change Your Pretty Permalinks or Add Custom Rewrite Rules

Permalinks are the permanent URLs to posts, categories, and other sections of your website. They generally look like this by default:

URLs should be permanent and never change — hence the name permalink. When you’re first configuring your site though, you can modify them from the default structure. Some of the values you can use include:

%year% The year of the post, four digits, for example 2004
%monthnum% Month of the year, for example 05
%day% Day of the month, for example 28
%hour% Hour of the day, for example 15
%minute% Minute of the hour, for example 43
%second% Second of the minute, for example 33
%post_id% The unique ID # of the post, for example 423
%postname% A sanitized version of the title of the post (post slug field on Edit Post/Page panel). So “This Is A Great Post!” becomes this-is-a-great-post in the URI.
%category% A sanitized version of the category name (category slug field on New/Edit Category panel). Nested sub-categories appear as nested directories in the URI.


For VIP clients, if you’d like to customize these permastructs, you can use any of the following helper functions in your theme’s functions.php file:

 * Enable a custom permastruct, if the site wants to use one that's not the default (/yyyy/mm/dd/post-name/)
wpcom_vip_load_permastruct( '/%category%/%postname%/' );

 * Enables a custom or no category base, if the site wants to use one that's not the default (/category/)
wpcom_vip_load_category_base( 'section' );

 * Enables a custom or no tag base, if the site wants to use one that's not the default (/tag/)
wpcom_vip_load_tag_base( 'topic' );

If you need to test this in a local environment, instead use the wp rewrite structure command from WP CLI.

Your theme’s rewrite rules are flushed automatically on every deploy (and when you switch themes), so you only need to worry about committing this change to the repository.

Custom rewrite rules can also be committed to your theme and incorporated in this manner.

There is no need to ever call flush_rewrite_rules() on WordPress VIP.

Rewrite Rules Inspector


A handy tool for testing and debugging your permastructs is the rewrite rules inspector. Enabled by default on VIP, this plugin features a complete list of your rules and a URL-matching tool to test your URLs. To find the inspector, visit VIP -> rewrite rules.

Note: if the inspector ever shows all or some of your rules in red and issues the “some rules are missing” error, click the flush rules button, and your rules should fully refresh.


Writing Bin Scripts

Occasionally, you may find you need to access or transform data on your site. If it’s more than a dozen posts affected, it’s often more efficient to write what we call a “bin script.” In writing a bin script, you can easily change strings, assign categories, or add post meta across hundreds or thousands of posts. However, with great power comes great responsibility — any small mistake you make with your logic could have negative repercussions across your entire dataset.

Here are some guidelines we’d encourage you to follow when writing a bin script.


To keep your scripts as lean and mean as possible, we highly encourage you to leverage WP-CLI, an awesome community framework.

When using WP-CLI, all you’ll need to write for your bin script is what’s called a “command.” Your command accepts zero or more arguments and performs a bit of logic. You won’t need to switch to the proper blog, handle arguments, etc., as all of that is done automatically.

Check out the great documentation on how to write a command. When you write commands for VIP, there are a few things to keep in mind:

  • You should extend the WPCOM_VIP_CLI_Command class provided in the development helpers, which includes helper functions like stop_the_insanity(). Do this instead of extending WP_CLI_Command.
  • Make sure you require the file that contains your new command in your functions.php file
  • Make sure you only include the command if WP_CLI is defined and true

Here’s an example of what might be in your functions file:

// CLI scripts
if ( defined( 'WP_CLI' ) && WP_CLI ) {
	require_once MY_THEME_DIR . '/inc/class-mycommand1-cli.php';
	require_once MY_THEME_DIR . '/inc/class-mycommand2-cli.php';

Once you’ve written your command and tested it throughly in your local environment, you can commit it to your theme.

When you’ve done so, open a ticket with us with explanation of what you’re trying to accomplish. We’ll review, test, and run it.

Best Practices

Again, it can be easy to make a minor mistake with your script that causes a lot of pain. We encourage you to do the following:

  • Comment well and provide clear usage instructions. It’s important to be very clear about what each part is doing and why — commenting each step of your logic is a good sanity check. Comments are especially helpful when something maybe doesn’t work as intended and we need to debug to figure out why.
  • If your script is calling wp_update_post() or importing posts, make sure to define( 'WP_IMPORTING', true ); at the top of your subcommand. This will ensure only the minimum of extra actions are fired.
  • Allow for varying levels of verbosity. Similarly, provide a summary at the end with all results of the script.
  • It’s a good idea to default your script to do a test run without affecting live data. Add an argument to allow a “live” run. This way, we can compare what the actual impact is versus the expected impact.
    A good way to do this is to do:

    $dry_mode = ! empty ( $assoc_args['dry-run'] );
    if( ! $dry_mode ) {
    	WP_CLI::line( " * Removing {$user->user_login} ( {$user->ID} )... " );
    	$remove_result = remove_user_from_blog( $user->ID, $blog_id );
    	if ( is_wp_error( $remove_result ) ) {
    		$failed_to_remove[] = $user;
    } else {
    	WP_CLI::line( " * Will remove {$user->user_login} ( {$user->ID} )... " );

    If your code modifies existing data we will ask for a dry run option so that we can confirm with you that things are good

  • Check your CLI methods have the necessary arguments. WP CLI passes 2 arguments, $args and $assoc_args to each command, you’ll need these to implement dry run options
  • If you’re modifying lots of data on a live site, make sure to include sleep() in key places. This will help with load associated with cache invalidation and replication. We also recommend using the WPCOM_VIP_CLI_Command methods stop_the_insanity() to clear memory after having processed 100 posts. If you are processing a large number of posts using the start_bulk_operation() and end_bulk_operation() class methods to disable functionality that is often problematic with large write operations.
  • Direct Database Queries will probably break in unexpected ways. Use core functions as much as possible, using direct SQL queries, (specifically those that do UPDATES or DELETES) will cause the caches to be invalid. In some cases if a direct SQL query is required, only do SELECT. Do any write operation using the core WordPress functionality. You may want to remove certain hooks from wp_update_post or do other actions to get the desired behaviour. In some rare contexts, a direct SQL query could be a better choice, but it must be followed by clean_post_cache().
  • When in doubt, ask us!


How do I modify all the posts?

Without a no-LIMIT query, it can be confusing how you would modify all your posts. The problem is that a no-LIMIT query just won’t work in most situations. If the query takes longer than 30 seconds, it will timeout and fail. The solution is use smaller queries and page through the results.

For example:


class Test_CLI_Command extends WPCOM_VIP_CLI_Command {

	 * CLI command that takes a metakey (required) and post category (optional)
	 * and publishes all pending posts once they have have had their metakeys updated.
	 * @subcommand update-metakey
	 * @synopsis --meta-key=<metakey> [--category=<category>] [--dry-run]
	public function update_metakey( $args, $assoc_args ) {
		$this->start_bulk_operation(); // Disable term counting, Elasticsearch indexing, and PushPress.

		$posts_per_page = 100;
		$paged          = 1;
		$count          = 0;

		// Meta key value is required, otherwise an error will be returned.
		if ( isset( $assoc_args['meta-key'] ) ) {
			$meta_key = $assoc_args['meta-key'];
		} else {
			 * Caution: calling WP_CLI::error stops the execution of the command.
			 * Use WP_CLI::error only in case you want to stop the execution. Use
			 * WP_CLI::warning or WP_CLI::line for non-blocking errors.
			WP_CLI::error( 'Must have --meta-key attached.' );

		// Category value is optional.
		if ( isset( $assoc_args['category'] ) ) {
			$cat = $assoc_args['category'];
		} else {
			$cat = '';

		// If --dry-run is not set, then it will default to true.
		// Must set --dry-run explicitly to false to run this command.
		if ( isset( $assoc_args['dry-run'] ) ) {
			 * passing `--dry-run=false` to the command leads to the `false` value being
			 * set to string `'false'`, but casting `'false'` to bool produces `true`.
			 * Thus the special handling.
			if ( 'false' === $assoc_args['dry-run'] ) {
				$dry_run = false;
			} else {
				$dry_run = (bool) $assoc_args['dry-run'];
		} else {
			$dry_run = true;

		// Let the user know in what mode the command runs.
		if ( $dry_run ) {
			WP_CLI::line( 'Running in dry-run mode.' );
		} else {
			WP_CLI::line( 'We\'re doing it live!' );

		do {
			$posts = get_posts( array(
				'posts_per_page'   => $posts_per_page,
				'paged'            => $paged,
				'category'         => $cat,
				'post_status'      => 'pending',
				'suppress_filters' => 'false',

			foreach ( $posts as $post ) {
				if ( ! $dry_run ) {
					update_post_meta( $post->ID, $meta_key, 'true' );
					wp_update_post( array( 'post_status' => 'publish' ) );

			// Pause.
			WP_CLI::line( 'Pausing for a breath...' );
			sleep( 3 );

			// Free up memory.

			 * At this point, we have to decide whether or not to increase the value of $paged
			 * variable. In case a value which is being used for querying the posts (like post_status
			 * in our example) is being changed via the command, we should keep the WP_Query starting
			 * from the beginning in every iteration. If the any value used for querying the posts
			 * is not being changed, then we need to update the value in order to walk through all the posts.
			// $paged++;

		} while ( count( $posts ) );

		if ( false === $dry_run ) {
			WP_CLI::success( sprintf( '%d posts have successfully been published and had their metakeys updated.', $count ) );
		} else {
			WP_CLI::success( sprintf( '%d posts will be published and have their metakeys updated.', $count ) );
		$this->end_bulk_operation(); // Trigger a term count as well as trigger bulk indexing of Elasticsearch site.

	 * CLI command that takes a taxonomy (required) and updates terms in that
	 * taxonomy by removing the "test-" prefix.
	 * @subcommand update-terms
	 * @synopsis --taxonomy=<taxonomy> [--dry_run]
	public function update_terms( $args, $assoc_args ) {
		$count = 0;

		$this->start_bulk_operation(); // Disable term counting, Elasticsearch indexing, and PushPress.

		// Taxonomy value is required, otherwise an error will be returned.
		if ( isset( $assoc_args['taxonomy'] ) ) {
			$taxonomy = $assoc_args['taxonomy'];
		} else {
			 * Caution: calling WP_CLI::error stops the execution of the command.
			 * Use WP_CLI::error only in case you want to stop the execution. Use
			 * WP_CLI::warning or WP_CLI::line for non-blocking errors.
			WP_CLI::error( 'Must have a --taxonomy attached.' );

		// If --dry-run is not set, then it will default to true.
		// Must set --dry-run explicitly to false to run this command.
		if ( isset( $assoc_args['dry-run'] ) ) {
			 * passing `--dry-run=false` to the command leads to the `false` value being
			 * set to string `'false'`, but casting `'false'` to bool produces `true`.
			 * Thus the special handling.
			if ( 'false' === $assoc_args['dry-run'] ) {
				$dry_run = false;
			} else {
				$dry_run = (bool) $assoc_args['dry-run'];
		} else {
			$dry_run = true;

		// Let he user know in what mode the command runs.
		if ( $dry_run ) {
			WP_CLI::line( 'Running in dry-run mode.' );
		} else {
			WP_CLI::line( 'We\'re doing it live!' );

		$terms = get_terms( array( 'taxonomy' => $taxonomy ) );

		foreach ( $terms as $term ) {
			if ( ! $dry_run ) {
				wp_update_term( $term->term_id, $term->taxonomy, array(
					'name' => str_replace( 'test ', '', $term->name ),
					'slug' => str_replace( 'test-', '', $term->slug ),
				) );

		$this->end_bulk_operation(); // Trigger a term count as well as trigger bulk indexing of Elasticsearch site.

		if ( false === $dry_run ) {
			WP_CLI::success( sprintf( '%d terms were updated.', $count ) );
		} else {
			WP_CLI::success( sprintf( '%d terms will be updated.', $count ) );

WP_CLI::add_command( 'test-command', 'Test_CLI_Command' );

Creating Cache Groups with vary_cache_on_function

The Scenario

  • Batcache speeds up by reusing one response for many requests.
  • A typical URL is a resource that provides a consistent response. This is easy to cache.
  • For some URLs, the responses vary depending on the request. This is harder to cache.
  • Variants created for logged-in users are not cached at all; users always get fresh responses.
  • Variants created by inspecting other things, such as the User Agent, must be cached separately.
  • Failure to separately cache these variants results in wrong responses. This is bad.

Example Problem

Imagine you want to use Feedburner to serve your feeds. You want to redirect everyone requesting to load it from instead. You set up your blog to redirect these requests. Then your Feedburner feed breaks because when Feedburner requests your feed, you refuse to give it the data, redirecting it to along with everyone else.

So you make an exception: redirect everyone except Feedburner. You inspect the User-Agent header and if it says “feedburner” you don’t redirect; you give Feedburner the feed. This is what the Feedburner plugin accomplishes. Now your feed works as designed and everyone gets what they are supposed to get. Except sometimes they don’t.

Now you’re running into a caching problem. We cache feeds. More correctly, we cache the responses generated by feed URLs so we can serve them more rapidly for subsequent requests. A redirect is also a response and it happens to be one of our favorite kinds of responses to cache.

The problem is that you have created a variant — the conditional redirect — without telling Batcache about it. Being unaware that your feed URL has several possible responses, Batcache merrily gives everyone whichever response is created for the first request after every cache expiration interval — maybe the redirect, maybe the feed — and it doesn’t care who gets which.

Hidden Mechanism

You have to tell the cache that you are serving a variant. But it’s not that simple because what you really have to tell the cache is how you knew that you were going to serve that variant. It needs to know this because its job is to receive requests and send correct responses. And it has to do that without loading themes, plugins, or more than about 2% of WordPress. That is why Batcache is so fast.

So if your URL is going to serve variants to anyone who is not logged in, you must inform Batcache how to determine which response goes with which request. That means giving it some logic to execute on its own, without help from your theme or plugins.

Example Problem Solved

Below is the standard logic to determine whether to serve the feed or the redirect. It is a variant determiner. You just didn’t call it that before.

function is_feedburner() {
    return (bool) preg_match("/feedburner|feedvalidator/i", $_SERVER["HTTP_USER_AGENT"]);

To make this logic available to Batcache, you have to give it a copy.

function is_feedburner() {
    if ( function_exists( 'vary_cache_on_function' ) ) {
            'return (bool) preg_match("/feedburner|feedvalidator/i", $_SERVER["HTTP_USER_AGENT"]);'
    return (bool) preg_match("/feedburner|feedvalidator/i", $_SERVER["HTTP_USER_AGENT"]);

See what we did there? We put the logic in a string and sent it to Batcache. Now the cache knows how to determine which variant goes with which request.

Solution Explained

The function vary_cache_on_function tells Batcache that the URL currently being served has variants, and how to determine them. It takes one argument, a string that will eventually be passed to create_function. (Click and get acquainted if you are not already. It can be tricky.) This is how your code will be used:

$fun = create_function('', $your_code);
$value = $fun();

This is a lightweight, fast way for Batcache to execute your code without loading your theme and plugins. But it’s not for lightweights. If you are keen on PHP security you might have noticed that any number of very small typos in your code would make it possible for anyone on the internet to execute arbitrary code on your blog, which would be both embarrassing and dangerous. This is equally true of all PHP programming but if you find create_function difficult, please ask for help.

More Mechanism

Very early in the code execution, long before WordPress has had time to figure out which blog was requested or who requested it, before it can include theme or plugin scripts, Batcache swoops into action and inspects the request. With only the superglobals to guide it ($_SERVER, $_GET, $_COOKIE) the cache searches for a pre-generated response. If it finds your “vary” code, it executes that and uses the result to narrow the search.

When Batcache finds a valid response for the current request, it serves the response and halts execution. That typically takes a few milliseconds which is quite fast. When Batcache finds none, the page must be generated from scratch. Batcache becomes dormant and waits until the response is complete. Then, reborn as an output buffer handler, it swoops back in, runs your code, adds the result to the metadata of the response, caches it, and releases the response to the client who requested it.

Common example

If you want to show a specific page to a common group of countries, (for example show a cookie notice on all EU countries) the initial solution might seem to be using

wpcom_geo_add_location( 'fr' );
wpcom_geo_add_location( 'uk' );
... etc

But this will actually create a separate cache bucket for each region. greatly diminishing the cache hit % in European countries.

A better way would be to create 2 separate buckets. One for EU and one for the rest of the world like such:

if ( function_exists( 'vary_cache_on_function' ) ) {
    'if ( isset( $_SERVER["GEOIP_COUNTRY_CODE"] ) && in_array( strtolower( $_SERVER["GEOIP_COUNTRY_CODE"] ), ["be", "bg", "cz", "dk", "de", "ee", "ie", "el", "es", "fr", "gb", "hr", "it", "cy", "lv", "lt", "lu", "hu", "mt", "nl", "at", "pl", "pt", "ro", "si", "sk", "fi", "se", "uk"], true ) ) {
        return true;
    } else {
        return false;

Other Problems

We haven’t thought of all the ways Batcache can break your variants. There are lots of good reasons to create variants: conditional redirects, different markup for different user agents, serving new features to specific IPs for testing, etc. You get the idea. If you plan on varying the response for any page that will likely cached, you should use vary_cache_on_function.


  • Your code is subject to the same limitations as Batcache: it can only use core PHP functions and inspect superglobals. While it is technically possible to gain functions by including scripts within your code, this would probably defeat the purpose of keeping the cache as speedy as possible. Avoid this, and certainly don’t do it without consulting a site administrator. They can also tell you how to disable the cache when necessary.
  • Batcache executes your “vary” code only during requests for the exact URL where you called vary_cache_on_function. Please limit your use of that function to URLs that actually have variants. That will help make every page load fast.
  • Get acquainted with create_function. Use single quotes and double quotes appropriately. Test thoroughly before committing or deploying code. If you aren’t sure, ask a site administrator for help.
  • This is worth repeating: when using create_function it is extremely easy to accidentally open the door to arbitrary code execution from the internet. Test, ask others to review your code, and test again.

Batcache uses Batcache to store and serve cached versions of rendered pages. Batcache uses memcached as its storage and is aimed at preventing a flood of traffic from breaking your site. It does this by serving old pages to new users. The caching is introduced after the same page has been requested at least 4 times in 5 minutes. This reduces the demand on the web server CPU and the database. It also means some people may see a page that is up to 5 minutes old.

Who receives a cached pageview?

By default, all new users receive a cached pageview.

New users are defined as anybody who hasn’t interacted with your domain—once they’ve left a comment or logged in, their cookies will ensure they get fresh pages. People arriving from Reddit won’t notice that the comments are a minute or two behind, but they’ll appreciate your site being up.

Cache Groups

We bucket users into different cache groups depending on the type of user they are. This means that you can vary the content you serve to each of these groups without worrying about cache collisions, as long as you use our helper functions.

* Logged-in user or commenter (no cache)
* Desktop
* Smartphone
* Dumbphone
* iPad
* Tablet


Note that most URLs with query strings are automatically exempt from Batcache caching. (Query string variables tend to signal dynamic input that informs dynamic output, so we provide an uncached result every time.) This can be undesirable in many cases as popular pages linked to with query strings can significantly reduce the effectiveness of our caching setup and can affect the overall performance of your site.

These query string parameters are an exception to this, and if a URL only contains one or more of these, it will still be subject to Batcache:

  • hpt
  • eref
  • iref
  • fbid
  • om_rid
  • utm
  • utm_source
  • utm_content
  • utm_medium
  • utm_campaign
  • utm_term
  • fb_xd_bust
  • fb_xd_fragment
  • npt
  • module
  • iid
  • cid
  • icid
  • ncid
  • snapid
  • hootPostID
  • _

Note that these query-string parameters will be removed from requests prior to processing, and thus they will not be accessible during processing of requests.

Hashbang URLs are cached based on the part of the URL prior to the hashbang. Any AJAX-originated HTTP requests based on hashbang state information may be subject to caching as well.

Avoid server-side operations

Because Batcache caches fully rendered pages, per-user interactions on the server-side can be problematic. This means usage of objects/functions like $_COOKIE, setcookie, $_SERVER['HTTP_USER_AGENT'], and anything that’s unique to an individual user cannot be relied on as the values may be cached and cross-pollution can occur.

In most cases, any user-level interactions should be moved to client-side using javascript.

In some cases, we can help you set up Batcache variants if you’re limiting your interactions to a small set of distinct groups (e.g. serve different content for users depending on whether the cookie “customer-type” is set, or equals “paid” or “pending”). Please get in touch if this something you’re interested in setting up.

Additional Resources

Localizing & Translating Themes

For localizing and translating themes, you can use the built-in i18n features available in WordPress.

Below, we’ve outlined a few things to keep in mind.

Using the correct locale

language files need to be in the format of,, and not or

$textdomain = true === WPCOM_IS_VIP_ENV ? 'default' : 'vip';

if( file_exists( $mofile ) )
         load_theme_textdomain( $textdomain, $mofile );

// __( 'My translatable string', 'vip' );

Translation conflicts

If the translations in your theme conflict with existing translations on, you have two options:

  • If your translation is better/more accurate, you can submit it via our GlotPress instance
  • Use the _x and friends (_ex, _nx, etc.) with an added context so that translations in your theme are given priority over others.

Uncached Functions

WordPress core has a number of functions that, for various reasons, are uncached, which means that calling them will always result in an SQL query. Below, we outline some of these functions. We have a helper file with cached versions of some of the functions, which is automatically available to you.

  • get_posts()
    • Unlike WP_Query, the results of get_posts() are not cached via Advanced Post Cache used on the VIP platform.
    • Use WP_Query instead, or set 'suppress_filters' => false.
    • When using WP_Query instead of get_posts don’t forget about setting ignore_sticky_posts and no_found_rows params appropriately (both are hardcoded inside a get_posts function with value of true )
  • wp_get_recent_posts()
    • See get_posts()
  • get_children()
    • Similar to get_posts(), but also performs a no-LIMIT query among other bad things by default. Alias of break_my_site_now_please(). Do not use. Instead do a regular WP_Query and make sure that the post_parent you are looking for is not 0 or a falsey value. Also make sure to set a reasonable posts_per_page, get_children will do a -1 query by default, a maximum of 100 should be used (but a smaller value could increase performance)
  • term_exists()
    • Use wpcom_vip_term_exists() instead
  • get_page_by_title()
  • get_page_by_path()
    • Use wpcom_vip_get_page_by_path() instead
  • url_to_post_id() and url_to_postid()
    • Use wpcom_vip_url_to_postid() instead
  • count_user_posts()
    • Use wpcom_vip_count_user_posts() instead.
  • wp_old_slug_redirect()
    • Use wpcom_vip_old_slug_redirect() instead.
  • get_adjacent_post()get_previous_post()get_next_post(), previous_post_link(), next_post_link()
    • Use  wpcom_vip_get_adjacent_post() instead.
  • attachment_url_to_postid()
    • Use  wpcom_vip_attachment_url_to_postid() instead.
  • wp_oembed_get()
    • Use wpcom_vip_wp_oembed_get() instead.

Ready to get started?

Drop us a note.

No matter where you are in the planning process, we’re happy to help, and we’re actual humans here on the other side of the form. 👋 We’re here to discuss your challenges and plans, evaluate your existing resources or a potential partner, or even make some initial recommendations. And, of course, we’re here to help any time you’re in the market for some robust WordPress awesomeness.