Skip to content

VIP warnings

Issues reported as a VIP Warning should not be pushed to a production server unless a specific use case makes it acceptable. Some common issues reported as VIP warnings are listed below.

*_meta as a hit counters

Do not use meta (post_meta, comment_meta, etc.) to track counts of things (e.g. votes, page views, etc.). It is not performant, and will not work as expected because of caching and race conditions on high-volume sites.

In general, avoid counting or tracking user events within WordPress. Instead, consider using a JavaScript-based solution paired with a dedicated analytics service (such as Google Analytics).

Ajax calls on every page load

Making GET and POST requests to admin-ajax.php on every page load or any page load without user input will cause performance issues and need to be rethought. If you have questions, we would be happy to help work through an alternate implementation.

Arbitrary JavaScript and CSS stored in options or meta

To limit attack vectors via malicious users or compromised accounts, arbitrary JavaScript cannot be stored in options or meta and then output as-is.

CSS in options or meta should also generally be avoided, but if necessary, it’s a good idea to sanitize it properly.

Commented out code, debug code or output

VIP themes should not contain debug code and should not output debugging information. That includes the use of functions that provide backtrace information, such as wp_debug_backtrace_summary() or debug_backtrace(). If you’re encountering an issue that can’t be debugged in your development environment, we’ll be glad to help troubleshoot it with you. The use of commented-out code should be avoided. Having code that is not ready for production on production is bad practice and could easily lead to mistakes while reviewing (since the commented-out code might not have been reviewed and the removing of a comment might slip in accidentally).

Cron schedules less than 15 minutes or expensive events

Overly frequent cron events (anything less than 15 minutes) can significantly impact the site’s performance, as can cron expensive events.

Custom API endpoints without permissions callback

For custom API routes that perform writes or read private data, registering a valid permissions callback is recommended to ensure that no data is exposed or manipulated. 

Custom roles

For best compatibility between environments and added security, custom user roles and capabilities must be managed via our helper functions.

Make sure to also validate and sanitize submitted values from users using the sanitize callback in the register_setting call.

Custom wp_mail headers

Every time you want to create custom headers using user-supplied data (e.g. “FROM” header), make sure you’re using filters provided by WordPress for you. See wp_mail_from() and wp_mail_from_name()

Direct database queries

Thanks to the WordPress API, it is rare to need to query database tables directly. Using WordPress APIs rather than rolling custom functions ensures compatibility with past and future versions of WordPress and PHP. It also makes code reviews go more smoothly because we know we can trust the APIs. Visit our page covering database queries for more information.

Additionally, direct database queries bypass internal caching. If necessary, you should evaluate the potential performance of these queries and add caching if needed.  Any queries modifying database contents may also put the object cache out of sync with the data, causing problems.

Encoding values used when creating a URL or passed to add_query_arg()

add_query_arg() is a really useful function, but it might not work as intended.
The values passed to it are not encoded, meaning that passing:

$m_yurl = 'admin.php?action=delete&post_id=321';
$my_url = add_query_arg( 'my_arg', 'somevalue&post_id=123', $my_url );

You would expect the url to be:

But in fact it becomes:

Using rawurlencode() on the values or variables passed to the query string prevents this and parameter hijacking.

eval() and create_function()

Both these functions can execute arbitrary code that’s constructed at run time, which can be created through difficult-to-follow execution flows. These methods can make your site fragile because unforeseen conditions can cause syntax errors in the executed code, which becomes dynamic. A much better alternative is an Anonymous Function, which is hardcoded into the file and can never change during execution.

If there are no other options than to use this construct, pay special attention not to pass any user-provided data into it without properly validating it beforehand.

Expensive 404 pages

The 404 page needs to be one of the fastest on the site, as it is only cached for 10 seconds. That means that a traffic spike from a broken link can cause performance and even availability problems if the 404 page is doing expensive db lookups.

Flash (.swf) files

Flash (.swf) files are not advisable on the WordPress VIP Platform, as they often present a security threat (largely due to poor development practices or due to bugs in the Flash Player) and vulnerabilities that are difficult to detect and secure.

Front-end database writes

Writing to the database on the front end is highly discouraged. It typically will not work as expected because most pageviews are cached. It can also cause a significant load on the database if many requests are written to it. We generally recommend avoiding functions that write to the database on the front.

Functions that use JOINS, taxonomy relation queries, -cat, -tax queries, subselects or API calls

Close evaluation of the queries is recommended as these can be expensive and lead to performance issues. Queries with known problems when working with large datasets:

  • category__and, tag__and, tax_query with AND
  • category__not_in, tag__not_in, and tax_query with NOT IN
  • tax_query with multiple taxonomies
  • meta_query with a large result set (e.g. looking for only posts with a thumbnail_id meta on a large site, looking for posts with a specific meta value on a key)

Generating email

To prevent issues with spam, abuse or other unwanted communications, your code should not generate or allow users to generate email messages to site users or user-supplied email addresses. That includes mailing list functionality, invitations to view or share content, notifications of site activity, or other messages generated in bulk. Where needed, you can integrate third-party SMTP or ESP (Email Service Provider) services that allow sharing of content by email, as long as they don’t depend on the VIP infrastructure for message delivery. If you only need to send out a few emails to admins or a specific email address, not in bulk amounts, you can use the built-in wp_mail() functionality.

Ignore development-only files

If it’s feasible within your development workflow, we ask that you .gitignore any files that are used exclusively in the local development of your theme, including but not limited to node_modules, .svnignore, config.rb, sass-cache, grunt files, PHPUnit tests, etc.

Including files with untrusted paths or filenames

locate_template(), get_template_part(), and sometimes include() or require() are typically used to include templates. If your template name, file name or path contains any non-static data or can be filtered, you must validate it against directory traversal using validate_file() or by detecting the string “..”

Minified JavaScript files

The minified JavaScript files should also be committed with changes to their unminified counterparts. Minified files cannot be read for review and are much harder to work with when debugging issues.

Mobile Detection

When targeting mobile visitors, use jetpack_is_mobile() instead of wp_is_mobile(). It is more robust and works better with full-page caching.

function jetpack_is_mobile( $kind = 'any', $return_matched_agent = false )

Where kind can be:

  • smart
  • dumb
  • any

You can also use:

  • Jetpack_User_Agent_Info::is_ipad()
  • Jetpack_User_Agent_Info::is_tablet()

Views from mobile devices are cached, and to make sure things work as expected, please let us know before including any additional server-side logic (PHP) in your code.

These are loaded automatically.

No LIMIT queries

Using posts_per_page (or numberposts) with the value set to -1 or an unreasonably high number or setting nopaging to true opens up the potential for scaling issues if the query ends up querying thousands of posts.

You should always fetch the lowest possible number that still gives you the number of acceptable results. Imagine that your site grows over time to include 10,000 posts. If you specify -1 for posts_per_page, you’ll query with no limit and fetch all 10,000 posts every time the query runs, destroying your site’s performance. If you know you’ll never have more than 15 posts, then set posts_per_page to 15. If you think you might have more than 15 that you’d want to display but doubt it’d hit more than 100 ever, set the limit to 100. If it gets much higher than that, you might need to rethink the page architecture a bit.

Not using the settings API

Instead of handling the output of settings pages and storage yourself, use the WordPress Settings API as it handles a lot of the heavy lifting for you, including added security.

Prefixing functions, constants, classes, and slugs

The best practice in WordPress is to prefix everything. This applies to obvious things such as names of functions, constants, and classes, and also less obvious ones like post_type and taxonomy slugs, cron event names, etc.

Plugin registration hooks

register_activation_hook() and register_deactivation_hook() are not supported because of the way plugins are loaded on WordPress VIP using wpcom_vip_load_plugin().

reCaptcha for share by email

To protect against abuse of Jetpack’s share by email feature (aka Sharedaddy) it must be implemented along with reCaptcha. This helps protect against the risk of the network being seen as a source of email spam, which would adversely affect VIP sites. This blog post explains how to implement reCaptcha.

Relative file includes

Including files with relative paths may lead to unintended results. It’s recommended that all files are included with an absolute path.

Several functions are available to compose the file path, such as __DIR__ , dirname( __FILE__ ) , plugin_dir_path( __FILE__ ), or get_template_directory().

// Don't do this:
require_once 'file.php';
// Do this instead:
require_once __DIR__ . '/file.php';

Remote calls

Remote calls such as fetching information from external APIs or resources should rely on the WordPress HTTP API (no cURL) and should be cached. Examples of remote calls that should be cached are wp_remote_get(), wp_safe_remote_get() and wp_oembed_get(). For more information, review our page on retrieving remote data.

Removing the admin bar

Removing the WordPress admin toolbar is strongly discouraged, and it must not be removed for administrators or vip_support roles.

Return values not checked

When defining a variable through a function call, you should always check the function’s return value before calling additional functions or methods using that variable.

function wpcom_vip_meta_desc() {
    $text = wpcom_vip_get_meta_desc();
    if ( ! empty( $text ) ) {
        echo "<meta name='description' content='" . esc_attr( $text ) . "' />";

Safelisting values for input/output validation

When working with user-submitted data, try to accept data only from a finite list of known and trusted values where possible. For example:

$possible_values = array( 'a', 1, 'good' );
if ( ! in_array( $untrusted, $possible_values, true ) )
die( "Don't do that!" );

Serializing data

Unserialize has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data.

Skipping full-page caching

Varnish is used for page caching at the edge, which improves performance by serving end-users a page that comes directly from the nearest datacenter. The GET parameters is always cached and done individually per GET parameter.  Varnish will only respect the Vary headers for X-Country-Code and Accept but not forCookie.

Taxonomy queries that do not specify 'include_children' => false

Almost all taxonomy queries include 'include_children' => true by default.  This can have a very significant performance impact on code, and in some cases, queries will time out.  We recommend 'include_children' => false to be added to all taxonomy queries when possible.

In many instances where all posts in either a parent or child term are wanted, this can be replaced by only querying for the parent term and using a save_post() hook to determine a child term is added, and if so enforce that its parent term is also added. A one-time WP-CLI command might be needed to ensure previous data integrity.

Uncached functions

WordPress provides various functions that interact with the database, not all of which are cacheable. To ensure high performance and stability, please avoid using any of the functions listed on our Uncached Functions list.

Use wp_parse_url() instead of parse_url()

In PHP versions lower than 5.4.7, schemeless and relative URLs would not be parsed correctly by parse_url(). We, therefore, recommend that you use wp_parse_url() for backward compatibility.

Use wp_safe_redirect() instead of wp_redirect()

Using wp_safe_redirect(), along with the allowed_redirect_hosts filter, can help avoid any chances of malicious redirects within code.  It’s also important to remember to call exit() after a redirect so that no other unwanted code is executed.

Using __FILE__ for page registration

When adding menus or registering your plugins, make sure that you use a unique handle or slug other than __FILE__ to ensure that you are not revealing system paths.


$_REQUEST should never be used because it is hard to track where the data is coming from (i.e., was it POST, or GET, or a cookie), which makes reviewing the code more difficult. Additionally, it makes it easy to introduce sneaky and hard-to-find bugs, as any of the aforementioned locations can supply the data, which is hard to predict.  A much better practice is to be explicit and use either $_POST or $_GET instead.

Using bloginfo() without escaping

get_bloginfo() should be used instead of bloginfo() so that data can be properly late escaped on output. get_bloginfo() can also return multiple data types, be used in multiple places, and may need to be escaped with many different functions depending on the context. For example:

echo '<a href="' . esc_url( get_bloginfo( 'url' ) ) . '">' . esc_html( get_bloginfo( 'name' ) ) . '</a>';
echo '<meta property="og:description" content="' . esc_attr( get_bloginfo( 'description' ) ) . '">';

Using extract()

extract() should never be used because it is too opaque and difficult to understand how it will behave under a variety of inputs. It makes it too easy to unknowingly introduce new variables into a function’s scope, potentially leading to unintended and difficult debugging conflicts.

Using page templates instead of rewrites

A common “hack” in the WordPress community when requiring a custom feature to live at a vanity URL (e.g. /lifestream/) is to use a Page + Page Template. This isn’t ideal for numerous reasons:

  • It requires WordPress to do multiple queries to handle the lookup for the page and any additional loops you manually run through.
  • It impedes development workflow as it requires the page to be manually created in each environment and new developer machines as well.

Last updated: September 12, 2022