Skip to main content

Command Palette

Search for a command to run...

A Cache Bypass Matrix for WordPress: Protecting Checkout, Login, REST, and Authenticated Sessions

Updated
6 min readView as Markdown
C
CottonCloud is a Slovak webdesign and WordPress studio publishing evidence-backed code breakdowns on performance, custom themes, safe automation and QA.

Page caching is easy when every request is an anonymous visit to a public article. A real WordPress site is rarely that simple.

The same installation may serve a checkout, a login form, a customer account, REST endpoints, AJAX requests, preview links and ordinary public pages. Treating those requests as equivalent can expose another visitor's state, replay stale form output or make a dynamic action look successful when it never reached WordPress.

The safest way to design page-cache eligibility is not to start with “what can we cache?” Start with “what must we bypass?” Then allow caching only after every unsafe condition has been ruled out.

The decision matrix

The following matrix is a practical baseline for a full-page HTML cache. It is intentionally conservative and formatted as a list so it remains readable on narrow screens.

  • Unsafe method — bypass. Any request other than GET or HEAD, such as POST /checkout/, must reach the application.
  • WordPress admin or login route — bypass. /wp-admin/ and /wp-login.php are authenticated or operational UI.
  • REST, AJAX or cron — bypass. /wp-json/, admin-ajax.php and wp-cron.php are APIs or background operations, not public documents.
  • Checkout, cart or account route — bypass. /checkout/, /cart/ and /my-account/ depend on session and customer state.
  • Preview, search or feed — bypass by default. ?preview=true, /?s=cache and /feed/ are temporary, query-driven or not normal HTML.
  • Authentication or session cookie — bypass. Login, cart and commerce session cookies signal private or personalized state.
  • Arbitrary query string — bypass by default. A cache key that ignores ?coupon=... or another rendering parameter can return the wrong variant.
  • Anonymous, queryless public document — eligible. A normal page or article may proceed only after all bypass checks pass.

This matrix is not a promise that the final row should always be cached. It only says the request has passed the first safety boundary. The response still needs its own checks before it can be stored.

Evaluate bypass rules before cache lookup

Ordering matters. If code reads a cached file first and checks the request later, the bypass logic is already too late.

The control flow should look like this:

function cc_cache_request_is_eligible(): bool {
    $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');

    if (!in_array($method, ['GET', 'HEAD'], true)) {
        return false;
    }

    $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');

    $blocked_prefixes = [
        '/wp-admin/',
        '/wp-login.php',
        '/wp-json/',
        '/checkout/',
        '/cart/',
        '/my-account/',
    ];

    foreach ($blocked_prefixes as $prefix) {
        if (str_starts_with($uri, $prefix)) {
            return false;
        }
    }

    if (!empty($_GET)) {
        return false;
    }

    foreach (array_keys($_COOKIE) as $name) {
        if (preg_match('/wordpress_logged_in|comment_author|woocommerce|cart|session/i', $name)) {
            return false;
        }
    }

    return true;
}

This is deliberately a readable example, not a drop-in universal plugin. Production code should normalize paths carefully, account for subdirectory installations and multisite, and let integrations register their own exclusions. Cookie names also vary between plugins.

The important contract is the order:

  1. classify the request;
  2. return early on any unsafe signal;
  3. build a normalized cache key only for eligible traffic;
  4. read the cached response;
  5. otherwise boot WordPress normally.

Do not store every successful response

A request can be eligible at the start and still produce a response that should not be cached. Before writing HTML to disk or another cache backend, check at least:

  • the HTTP status is an explicitly allowed public status, normally 200;
  • the response content type is HTML;
  • WordPress did not set a private or session cookie;
  • response headers do not say no-store, private or otherwise prohibit storage;
  • the request did not become a logged-in, preview or error flow during execution;
  • the body is complete and above a sensible minimum size.

Fail closed. When a signal is missing or ambiguous, serve the live response and skip the cache write. A missed cache opportunity costs performance; a cached private response costs trust.

Make exclusions observable

A bypass system is much easier to operate when it explains its decision. In a safe diagnostic mode, emit a non-sensitive reason such as:

X-Page-Cache: BYPASS; reason=authenticated-cookie
X-Page-Cache: BYPASS; reason=unsafe-method
X-Page-Cache: MISS
X-Page-Cache: HIT

Do not include raw cookies, tokens, email addresses or complete query strings in headers or logs. A short reason code is enough to debug the branch without leaking visitor data.

These signals also turn QA into a repeatable test instead of a visual guess.

Test the negative paths first

Before measuring cache hit rate, verify that the dangerous paths never hit. A small release matrix should include:

Test Expected result
Anonymous public page, first request MISS, then a valid stored object
Same page, second anonymous request HIT with equivalent public HTML
Logged-in user on the same page BYPASS
POST request BYPASS
REST and AJAX requests BYPASS
Cart, checkout and account pages BYPASS
Preview and search requests BYPASS
Response setting a session cookie Served live and not stored
Purge followed by public request MISS, then a new object

Run the matrix on desktop and mobile paths, but do not automatically split cache keys by device. Device variants increase invalidation and correctness complexity. Add them only when the rendered HTML truly differs and the detection rule is stable.

Release with an inverse operation

Cache changes should ship with a rollback that is simpler than the forward change. Capture the active configuration, deployed code identity and exclusion list before switching. Then prepare an inverse operation that restores those exact preimages.

A safe cutover looks like this:

  1. snapshot the current cache rules and version;
  2. deploy the new classifier without deleting the old version;
  3. clear only the affected cache namespace;
  4. run the negative-path matrix before performance tests;
  5. verify public pages at real breakpoints;
  6. keep the previous version available until the observation window closes.

Do not call the release successful because a homepage became faster. It is successful only when public pages can hit the cache and sensitive paths reliably bypass it.

The principle behind the matrix

Performance work is a correctness problem before it is a speed problem. The strongest cache is not the one that stores the most pages; it is the one with an explicit, testable boundary between public content and stateful traffic.

We use this same safety-first model while developing Custom Performance Cache for WordPress. This article is published by CottonCloud, the team behind that product. The link is provided for readers who want to see the public feature scope; the matrix above stands on its own and can be implemented with any suitable cache architecture.