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-Controlvalue ispublic, 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
PURGEper affected URL. Thepostevent 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 configso 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
Hostheader. If the stack derives the address from it, check thehostsentry afterwp bb-cache config, or pass--url=https://example.comto the command. - To serve additional host names (a second domain mapped to the same site), add them with the
bb_cache_hostsfilter and then save the settings. - A
config.phpwritten before the allow-list existed has nohostskey. The self-heal keeps that empty list, which leaves the check off until the next settings save orwp 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.phpif itsversiondiffers (the self-heal). config.phpitself 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.