Skip to content

Hosting notes#

The page cache works wherever PHP can write to wp-content/cache/bb-cache and WordPress loads wp-content/advanced-cache.php. The notes below cover what differs per stack.

File permissions#

The PHP user must be able to write:

Path Needed for
wp-content/cache/bb-cache/ Cached pages, minified files, local fonts, config.php. Without it nothing is cached and an admin notice appears
wp-content/advanced-cache.php Installing the drop-in
wp-config.php Setting WP_CACHE. Optional: add define( 'WP_CACHE', true ); by hand if the file is read-only
.htaccess The Apache rewrite block. Optional: paste the block from the Tools tab by hand

On stacks where files are deployed by one system user and PHP runs as another (Bitnami images run PHP as daemon while SFTP uploads belong to bitnami), check group ownership and write permission on these paths after a deploy.

The drop-in finds the cache folder relative to its own file, not from an absolute path in config.php. This keeps it working on hosts where the command line and the web server see the document root under different absolute paths (bindfs or chroot setups).

Apache: .htaccess rules#

With htaccess_rules on, BB Cache writes a block to .htaccess, above the # BEGIN WordPress block, that lets Apache send a cached file without starting PHP. Responses served this way carry X-BB-Cache: HIT (htaccess).

With default settings on a site at the domain root the block reads:

# BEGIN BB Cache
# Serves pages straight from the BB Cache page files when it is safe to (GET, no query string, no session cookie).
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{HTTPS} on [OR]
RewriteCond %{HTTP:X-Forwarded-Proto} https
RewriteRule .* - [E=MT_SSL:-https]
RewriteCond %{HTTP_USER_AGENT} (Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera\ Mini|Windows\ Phone|Silk/|Kindle|webOS) [NC]
RewriteCond %{HTTP_USER_AGENT} !(iPad|Tablet) [NC]
RewriteRule .* - [E=MT_MOBILE:-mobile]
RewriteCond %{REQUEST_METHOD} GET
RewriteCond %{QUERY_STRING} =""
RewriteCond %{HTTP_COOKIE} !(wordpress_logged_in_|wp-postpass_|comment_author|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_) [NC]
RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/bb-cache/pages/%{HTTP_HOST}/$1/index%{ENV:MT_SSL}%{ENV:MT_MOBILE}.html -f
RewriteRule ^(.*)$ /wp-content/cache/bb-cache/pages/%{HTTP_HOST}/$1/index%{ENV:MT_SSL}%{ENV:MT_MOBILE}.html [L]
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "index(-https)?(-mobile)?\.html$">
Header set X-BB-Cache "HIT (htaccess)"
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>
</IfModule>
# END BB Cache

The block is generated from the settings:

  • The cookie condition is built from reject_cookies. Only entries made of letters, digits, _ and - are used, so a malformed entry cannot break the rewrite rules. If none qualifies, wordpress_logged_in_ is used.
  • The Cache-Control value is public, max-age=0, s-maxage=<cf_edge_ttl> when the edge TTL is set.
  • Paths follow the site's subdirectory and the content folder name.

Saving the settings rewrites the block. The Tools tab shows its state (ok, stale, absent, missing) and the current text, for pasting by hand when .htaccess is not writable. Deactivating the plugin removes the block.

What the block does and does not do, compared with the PHP drop-in:

Aspect .htaccess block PHP drop-in
Methods GET only GET only
Query strings Only requests with an empty query string. Everything else goes to PHP Marketing parameters ignored, allow-listed parameters cached as variants
Session cookies reject_cookies reject_cookies
reject_ua Not checked Checked
Host allow-list Not checked; a file must already exist under the request's host folder Checked
Lifespan Not checked. An old file is served until the hourly sweep, a purge or a PHP-served request removes it Checked on every request
HTTPS and mobile variants Yes Yes
WebP variant No. The non-WebP file is served when it exists Yes
Non-standard port No match (the folder name uses _ for :), so PHP serves Yes

If you rely on reject_ua, the WebP variant or an exact lifespan, leave the block off. The PHP drop-in is already fast: it runs before WordPress loads.

The block needs mod_rewrite; the two response headers need mod_headers. Without mod_headers the files are still served, without the X-BB-Cache and Cache-Control headers.

nginx#

nginx does not read .htaccess, so the rewrite block does not apply. Leave htaccess_rules off. If the site has no .htaccess file, the plugin writes nothing and reports the state as missing.

Nothing else changes: the PHP drop-in still serves cached pages before WordPress loads, and front-end responses carry the X-BB-Cache header. The plugin does not ship an nginx configuration.

Varnish purge#

Some managed hosts (Cloudways, for example) run Varnish in front of the web server. Set varnish_host to the address Varnish listens on, typically 127.0.0.1 on such hosts, and BB Cache repeats its purges there. Leave the setting blank on hosts without Varnish.

Page cache event Request sent
One URL purged PURGE <scheme>://<varnish_host><path> with the site's host name in the Host header
Everything purged PURGE <scheme>://<varnish_host>/.* with Host and X-Purge-Method: regex
  • The scheme is the scheme of the site address.
  • Requests are non-blocking with a 5 second timeout and without TLS verification. Failures are ignored, since Varnish may simply be switched off.
  • A post save sends one PURGE per affected URL. The post event that closes a post purge is ignored (before 0.3.0 it fell into the "everything" branch and emptied Varnish on every save).

Cached pages are sent with Vary: Accept-Encoding, Cookie (plus User-Agent and Accept when the mobile and WebP variants are on) and, without an edge TTL, Cache-Control: no-cache, must-revalidate.

Site address built from the Host header#

Some stacks define the site address dynamically. Bitnami's WordPress images, for example, build WP_HOME and WP_SITEURL in wp-config.php from the request's Host header. On such a site home_url() returns whatever host name the visitor sent.

The page cache has a host allow-list (hosts in config.php): the drop-in serves and stores only for the site's own host name, with and without www.. Without it, a request with a made-up Host header would create a cache folder for that host. If the list were derived from home_url() during an ordinary front-end request on such a stack, a visitor's Host header could decide the list and lock the real host out.

For that reason the list is derived only in trusted contexts:

  • a settings save by an administrator
  • activation and Tools → Reinstall advanced-cache.php and set WP_CACHE
  • WP-CLI (wp bb-cache config)

The version self-heal, which runs on a front-end request after an update, never derives the list. It carries over the list already in config.php.

Practical consequences:

  • After changing the site's domain, save the settings or run wp bb-cache config so the list follows.
  • On a stack with a dynamic site address, save the settings while using the admin under the canonical host name, and check the result with the command below.
  • Under WP-CLI there is no Host header. If the stack derives the address from it, check the hosts entry after wp bb-cache config, or pass --url=https://example.com to the command.
  • To serve additional host names (a second domain mapped to the same site), add them with the bb_cache_hosts filter and then save the settings.
  • A config.php written before the allow-list existed has no hosts key. The self-heal keeps that empty list, which leaves the check off until the next settings save or wp bb-cache config.

To see the list in use:

grep -A4 "'hosts'" wp-content/cache/bb-cache/config.php

A request for a host that is not on the list gets X-BB-Cache: MISS (host) and is never stored.

Opcache windows after updates#

PHP's opcache may keep running the previous version of a file for a while after it changes on disk (opcache.revalidate_freq; 60 seconds on some images, and indefinitely where opcache.validate_timestamps is off). After updating the plugin files:

  • For the length of that window, requests can still run the old code. The first request that runs the new code rewrites config.php if its version differs (the self-heal).
  • config.php itself is a PHP file that the drop-in includes, so a rewritten config can also take a moment to be picked up.
  • If the server never revalidates timestamps, reset opcache or reload PHP after a deploy.

Check after an update:

wp bb-cache status
grep "'version'" wp-content/cache/bb-cache/config.php
curl -s -o /dev/null -D - https://example.com/ | grep -i x-bb-cache

A separate opcache issue affects WP-CLI on some PHP builds with opcache.enable_cli on: declaring a WP-CLI command class during plugin load fails with "Cannot declare class WP_CLI". BB Cache avoids it by loading its command class only after WordPress has loaded.

Cloudflare and other CDNs#

See Cloudflare. For any CDN that caches HTML, note that the origin keeps separate variants per device and per WebP support, while a CDN normally keeps one copy per URL. Turn cache_mobile and cache_webp off in that case.