Technology

WordPress Full-Page Cache: Nginx, Varnish and LiteSpeed

A WooCommerce store can slow down just when traffic finally arrives. Full‑page caching can reduce the work needed to serve public pages during a rush, leaving more capacity for dynamic requests such as add-to-cart and checkout. Those personalized requests still need to bypass the shared page cache.

PHP may repeatedly generate the same public HTML under load. A server-level cache hit can reuse that response without invoking WordPress PHP or its database queries. Misses and bypasses still reach the application, so compare hit rate, latency, errors and resource use for representative page types before estimating a benefit.

In this guide, we’ll compare three full‑page caching options: Nginx FastCGI Cache, Varnish, and LiteSpeed Cache. We’ll cover where each fits, WooCommerce bypass rules, TTLs, purging strategies, and checks for personalized pages. The aim is faster public pages without caching checkout or account data; each setup still needs validation against the store’s routes and extensions.

What Full-Page Caching Does

A cache stores responses for later requests that are eligible to receive the same representation. The cache key identifies that representation; freshness rules determine how long it may be reused. A hit does not prove that the content belongs to the current visitor or that product information is sufficiently fresh: those are separate requirements.

On WordPress, that “first pour” is PHP generating your HTML. Full‑page caching saves that HTML response for reuse by visitors eligible for the same cached version. It’s different from object caching (like Redis), which stores reusable data objects rather than whole pages. Cache hits can reduce CPU work, database load, and time‑to‑first‑byte; the gain depends on the workload and hit rate.

Cart contents, accounts, coupons, currency and region-based taxes can make responses different between visitors. Start with an explicit set of public pages and bypass requests carrying cookies, authorization or query parameters. Supported ESI fragments are a separate integration option; they do not make whole cart, checkout or account pages suitable for shared caching.

Comparing Nginx, Varnish and LiteSpeed

These three options differ in deployment, cache-control logic, and WordPress integration.

Nginx FastCGI Cache stores responses from a FastCGI application such as PHP-FPM inside the existing web server. It can suit a stack already using Nginx, with no separate HTTP cache daemon. Correct read/write bypass, deployment-specific PHP handling and an invalidation integration remain necessary; speed depends on actual cache hits and bottlenecks.

Varnish Cache adds an HTTP cache in front of a web server. VCL can express request and response policy, but brings another service to operate and monitor. It connects to an HTTP backend, not directly to PHP-FPM’s FastCGI socket. Explicit WordPress/WooCommerce exclusions and a trusted proxy arrangement are required.

LiteSpeed Cache combines a compatible server cache engine with the WordPress plugin. Installing the plugin alone does not enable server page caching on every host. ESI needs a supporting layer such as LiteSpeed Web Server Enterprise, Web ADC or QUIC.cloud CDN; OpenLiteSpeed does not support ESI. Image services and crawler availability have separate service and server requirements.

Across all three, cache only responses whose reuse is authorized and sufficiently fresh. Begin with a narrow public-page policy; expand it only after checking the store’s routes, extensions, variants and invalidation behavior. A higher hit rate is not useful if it reuses the wrong visitor’s data.

WooCommerce Cache Exclusions and Freshness

Before we talk configs, let’s anchor the rules that keep money flowing without weirdness. Caching checkout responses can expose stale totals or another visitor’s data. The following guardrails are a starting point, not a substitute for checking the store’s custom routes and extensions.

1) Don’t cache the sensitive pages

Exclude the store’s actual, including localized, cart, checkout and account routes. Also exclude administration/login, REST and Store API, previews, payment callbacks and custom sensitive endpoints. The English paths /cart, /checkout and /my-account are examples. A conservative allowlist of known public URLs keeps unrecognized routes dynamic as well.

2) Bypass when these cookies exist

WooCommerce sets a few cookies that mean “this visitor is in a personalized state.” The ones that matter most are:

woocommerce_items_in_cart, woocommerce_cart_hash, wp_woocommerce_session_ and wordpress_logged_in_ are important signals, but password-protection, commenter and extension cookies can also affect HTML. The examples below bypass every Cookie or Authorization request. Such requests must neither read a shared cached response nor populate it; passing only one of those checks is insufficient.

3) Watch query strings that change state

State changes such as ?add-to-cart=, previews and payment callbacks must bypass shared caching. The examples bypass every query string, including reordered parameters, and only allow GET requests into page cache. HEAD and other methods remain dynamic. This sacrifices some hits while keeping the starting policy easier to verify; do not remove parameters until their application effects are understood.

4) Reasonable TTLs and safe staleness

Choose TTL from the accepted age of prices, stock and editorial content, then connect updates to invalidation. A ten-minute fresh TTL is not a ten-minute maximum age if stale serving or grace is also enabled. The examples add no stale-on-error or background-update policy. Before adding one, define the permitted stale interval across origin and CDN, and test refresh and outage behavior. Checkout must validate current prices and availability dynamically.

5) Purge strategy that mirrors how you publish

Invalidate the changed product, affected category/tag listings, homepage displays and every cached language, currency or other variant. Purge the other cache layers too. If dependencies are unknown, a broader purge may be appropriate, but plan for the resulting misses. Tag-based invalidation and BAN only work when the necessary metadata, authorization and application hooks are implemented.

Nginx FastCGI Cache: A Conservative Example

The following example starts with three explicitly reviewed public URLs. It allows only cookie-free, unauthenticated GET requests without query strings; all other routes remain dynamic. Replace the example product path, configured/localized exclusions, hostname, certificate paths, document root and PHP socket before testing. Retain the site’s existing private-file and upload-execution protections. This illustrates cache policy, not a complete hosting configuration.

Cache key and skip logic

Put these maps in the Nginx http context. The public-page map is deliberately restrictive; it is not a rule to cache every product or every route that lacks a known WooCommerce cookie. Paths with query strings do not match its exact entries. The sensitive-route map adds explicit exclusions, including common Turkish examples; use the actual routes configured in the store.

# Place in http context. List only reviewed, identical public HTML pages.
map $request_uri $not_public_page {
    default 1;
    ~^/$ 0;
    ~^/shop/$ 0;
    ~^/product/example-product/$ 0;
}
map $request_method $non_get {
    default 1;
    ~^GET$ 0;
}
map $http_cookie $has_cookie {
    default 1;
    "" 0;
}
map $http_authorization $has_authorization {
    default 1;
    "" 0;
}
# Add every configured/localized sensitive route; the allowlist is also required.
map $request_uri $sensitive_route {
    default 0;
    ~*^/(wp-admin|wp-login\.php|wp-json|wc-api|cart|checkout|my-account|sepet|odeme|hesabim)(/|\?|$) 1;
}

Cache Storage, HTTPS and PHP Handling

Place the following cache path and server blocks in the same http context. This version terminates HTTPS directly in Nginx and redirects HTTP to a fixed canonical host. A CDN/TLS-proxy deployment needs a separately verified trusted-scheme configuration. Keep the cache directory outside the web root with appropriate ownership; preserve the deployed site’s other security and routing rules.

# Place in http context alongside the maps. Adapt every deployment path.
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=WPFAST:10m inactive=60m use_temp_path=off;

server {
    listen 80;
    server_name example.com;
    return 308 https://example.com$request_uri;
}
server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;
    root /var/www/html;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS $https if_not_empty;
        fastcgi_pass unix:/run/php/php-fpm.sock;

        # Only the WordPress front controller can populate this page cache.
        set $not_front_controller 1;
        if ($fastcgi_script_name = /index.php) { set $not_front_controller 0; }
        fastcgi_cache WPFAST;
        fastcgi_cache_key "$scheme|$request_method|$host|$request_uri";
        fastcgi_cache_valid 200 10m;
        fastcgi_cache_lock on;
        fastcgi_cache_use_stale off;
        fastcgi_cache_background_update off;
        fastcgi_cache_bypass $not_public_page $non_get $has_cookie $has_authorization $sensitive_route $not_front_controller;
        fastcgi_no_cache $not_public_page $non_get $has_cookie $has_authorization $sensitive_route $not_front_controller;
        add_header X-Cache-Status $upstream_cache_status always;
    }
    # Retain the site's reviewed private-file, upload-execution and asset rules.
}

Both bypass and no-cache conditions are required. Keep normal upstream Cache-Control, Set-Cookie and Vary processing enabled; never use ignore-header directives merely to force hits. Ten minutes is the fallback freshness for eligible 200 responses; upstream cache headers can govern it. Also inspect upstream stale-if-error and stale-while-revalidate directives: response headers can authorize stale reuse, so verify their absence when testing the initial no-stale policy. Nginx’s official FastCGI purge feature is commercial. Open-source deployments need an appropriate third-party module or reviewed integration; a short TTL is delayed expiry, not immediate invalidation.

If you want to go deeper into the directives themselves, the official module docs are a good anchor: the Nginx FastCGI module reference.

What about microcaching?

A short TTL may be useful for public pages with frequent requests, but it only helps when enough requests reuse the same safe response. Choose the interval from the freshness requirement and measured load. Cache locking can reduce concurrent fill work; stale/background behavior is separate and should be added only with a defined age limit and tests. None of these replaces personal-response exclusions.

Varnish and LiteSpeed Configuration Choices

Varnish: the flexible guardian in front

The VCL example uses the same narrow public-page policy and retains all request cookies. Only cookie-free GET requests without authorization or queries can be eligible. Place Varnish Cache behind the trusted TLS terminator and restrict direct access to Varnish/backend. For this example, redirect public HTTP requests to HTTPS at the terminator and forward only requests received over HTTPS to Varnish. The built-in cache hash uses the URL and host, not the original connection scheme. The proxy must overwrite forwarded scheme headers using the actual connection, and the backend must accept that information only through the trusted proxy chain; do not accept arbitrary client-supplied scheme values.

vcl 4.1;
backend default {
    .host = "127.0.0.1";
    .port = "8080"; # HTTP web server, never a PHP-FPM/FastCGI socket
}
sub vcl_recv {
    # A trusted TLS proxy must overwrite Host/scheme headers before this hop.
    if (req.method != "GET" || req.http.Authorization || req.http.Cookie) {
        return (pass);
    }
    if (req.url ~ "[?]" || req.url ~ "(?i)^/(wp-admin|wp-login[.]php|wp-json|wc-api|cart|checkout|my-account|sepet|odeme|hesabim)(/|$)") {
        return (pass);
    }
    # Keep only reviewed public pages; everything else remains dynamic.
    if (req.url != "/" && req.url != "/shop/" && req.url != "/product/example-product/") {
        return (pass);
    }
    # Fall through to the built-in request policy and hash.
}
sub vcl_backend_response {
    set beresp.grace = 0s;
    set beresp.keep = 0s;
    if (bereq.uncacheable) { return (deliver); }
    if (beresp.status != 200 || beresp.http.Content-Type !~ "(?i)^text/html" ||
        beresp.http.Set-Cookie || beresp.http.Cache-Control ~ "(?i)(private|no-store|no-cache)" ||
        beresp.http.Surrogate-Control ~ "(?i)no-store" || beresp.http.Vary == "*") {
        set beresp.uncacheable = true;
        set beresp.ttl = 120s; # hit-for-miss marker, not reusable personalized HTML
        return (deliver);
    }
    if (beresp.ttl > 10m) { set beresp.ttl = 10m; }
    # Built-in policy still checks zero TTL and other cache controls.
}
sub vcl_deliver {
    if (obj.uncacheable) {
        set resp.http.X-Cache = "PASS";
    } else if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
}

Validate this example with the supported Varnish Cache package selected for deployment. PURGE/BAN request handling is not implemented here: add authenticated, limited management handling and invalidation metadata before relying on it. The Varnish VCL guide provides language and lifecycle context; consult the documentation for your installed version.

LiteSpeed Cache and WooCommerce Integration

LSCWP can coordinate server cache and WordPress/WooCommerce events on a compatible stack. ESI may isolate a supported mini-cart or other fragment as private, separately cached or uncached content. It requires server support and actual theme/plugin integration; switching on ESI does not discover every custom personal area or guarantee that cached fragments are always current.

Use the following checks as a starting policy and verify them against the plugin version and store configuration:

Verify that the actual Cart, Checkout and My Account page associations are correct and those pages bypass cache. Start with logged-in-user caching disabled; sharing a public variant by role does not isolate two customers’ account data. Enable mobile variants only where the theme or selected optimization feature needs them. Choose public TTLs from freshness needs, test product/listing invalidation, and keep action queries out. Enable ESI only for supported fragments with verified private/uncached behavior, TTL and invalidation.

When in doubt, lean on their docs to translate a checkbox into behavior: LiteSpeed Cache for WordPress – General guide.

For media variants, verify what each URL returns and how the CDN keys it. If one URL selects WebP/AVIF or a fallback based on Accept, preserve the appropriate Vary: Accept behavior for all relevant variants. Separate fixed-format URLs have different requirements. The presence of an optimization plugin does not by itself prove correct origin, CDN and browser caching.

Purge, CDN, and Testing: Keeping the Cache Honest

Once the core rules are in place, real‑world stability comes down to three habits: purge with purpose, align with your CDN, and test like you’re a stranger.

Purge strategy that makes sense

Connect product edits, stock events and publishing to the affected detail pages, category/tag archives, homepage displays and response variants. For Nginx, verify the installed purge integration; for Varnish, implement authorized invalidation handling and metadata; for LiteSpeed, verify the plugin’s chosen update/purge rules. A configured hook is insufficient until a controlled update shows fresh content at each relevant cache layer.

Use narrow invalidation when dependencies are known, but do not leave affected pages stale just to avoid a wider purge. Broad purges create more origin work; rate-limit optional warmups and monitor errors and capacity. Shorter TTLs limit ordinary freshness only and cannot compensate for missing personal-response exclusions.

Your CDN needs to play the same game

Align origin and CDN exclusions, variants and invalidation. Verify how each layer interprets Cache-Control and any supported Surrogate-Control; the latter is not universally honored. An origin bypass does not prevent an edge hit. ESI and additional HTML caches may conflict, so follow the integration’s documented support. The WordPress CDN and WooCommerce bypass guide examines those layers together.

Testing Cache Reads, Writes and User Flows

Before a change, record baseline metrics and retain the previous configuration. Test the new policy in isolated staging with real payment/email integrations disabled:

Use GET requests and inspect both headers and response bodies. Repeat a known public URL under the same conditions; a fresh eligible fill may show MISS then HIT, but a prewarmed object may start with HIT. A Set-Cookie/private/no-store response should remain uncached. Cache headers must be configured and tied to the correct layer; a CDN can replay an old origin-status header. HEAD behavior need not match GET.

Use two independent visitors and accounts. Check cart, checkout, account, coupons, shipping, taxes, currency, payment callbacks and both button/direct ?add-to-cart= flows. A cookie-bearing request must neither consume a public cached body nor replace the body later shown to an anonymous visitor. Merely seeing a cache-related header is not a failure: distinguish HIT, MISS, BYPASS/PASS and absent status.

Update a test product and check every affected page and variant at origin and CDN. Verify the promised invalidation timing; lowering TTL is not proof of immediate purge. Test expiry, backend errors and any intentional stale policy. Exercise JavaScript and fragment behavior on and off where the store supports those flows; confirm nonce expiry and account isolation rather than only page appearance.

Personalization and API Edge Cases

Currency, geolocation, tax and dynamic-price extensions can affect general-looking pages. Keep them bypassed until cache variants and permissions are explicitly verified. A role-based public variant is not a user-specific private cache. HTML mini-cart content needs supported fragment integration or bypass; do not assume every theme implements ESI or refreshes it safely.

Search/filter queries, health or analytics endpoints, REST and Store API routes need an explicit policy. The examples keep them outside the public allowlist. Add custom sensitive paths to the exclusion inventory, including localized page slugs. A cache setup that only tests English cart/checkout URLs is incomplete.

Keep admin-AJAX, webhook, cron and other application-operation endpoints outside the shared page cache. Non-GET/HEAD methods and GET actions must not be reused. Test API headers and responses without logging tokens, raw session cookies or personal payloads.

Choosing a Cache Policy for the Stack

Choose according to the existing stack, operating responsibility and measured workload. The following are deployment options, not customer benchmarks or guaranteed performance outcomes:

With Nginx and PHP-FPM, begin with a small public allowlist, both read/write bypass conditions and an appropriate freshness policy. Verify the actual PHP/TLS paths and preserve site security rules. Use an invalidation integration where updates must appear before TTL expiry; add stale behavior only after defining and testing its limits.

With Varnish, account for the extra HTTP hop, trusted TLS proxy, built-in cache policy and management endpoints. Start without grace if serving old product information is unacceptable. Explicitly test authorization, cookies, private/no-store and Set-Cookie responses, custom routes and invalidation before expanding cache scope.

With LiteSpeed, verify cache-engine availability, the plugin’s actual WooCommerce page associations and update rules. Keep cart, checkout and account pages excluded. Add supported ESI fragments only after checking private/uncached behavior and freshness. Plugin controls simplify configuration but do not replace user-flow tests.

Whichever stack is chosen, align CDN rules, record cache outcomes per layer and retain a rollback configuration. Compare application errors, latency, hit rates and resource use with the baseline. Correct prices, account separation and functioning transactions are acceptance criteria alongside performance.

Response Headers and Cache Warmups

WordPress headers that help

Personal and transactional responses should have an appropriate Cache-Control: no-store policy as well as route/session exclusions. For public HTML, distinguish shared-cache freshness from browser freshness; browser copies cannot be purged from the server. A policy such as Cache-Control: public, max-age=0, s-maxage=600 may suit an explicitly public response, subject to the deployed cache’s support. Do not add stale directives until the permitted interval is defined and tested.

Cache warmups (optional, not mandatory)

Optional warmups can reduce cold misses for a small set of public pages. Use cookie-free GET requests at a controlled rate and stop if error or resource thresholds are crossed. Do not crawl personalized/action URLs. LiteSpeed crawling depends on server support and enablement; additional variants can multiply the work. A warmer is not required for correctness.

Where to read more when you get curious

Keep the Nginx FastCGI reference, Varnish VCL guide and LiteSpeed Cache documentation available when adapting the setup. Validate syntax and cache behavior using the supported deployed versions, then test real WooCommerce flows and extensions separately in staging. Synthetic checks alone do not establish transaction correctness or production performance.

Keep Performance and Content Correctness Together

Full-page caching can reduce work for correctly identified public responses. It must preserve customer isolation and freshness requirements. Explicit eligibility, matched read/write exclusions, variants, invalidation and tests are the basis of a useful setup; choosing a cache product alone does not provide those guarantees.

Evaluate the result using comparable measurements before and after the change. Lower origin CPU or TTFB may be useful, but check checkout errors, displayed prices and personal responses too. A synthetic HIT/MISS test establishes only the behavior exercised; it cannot predict every theme, extension or traffic pattern.

Start small, adapt the examples to the actual site, validate syntax, and test independent sessions in staging. Enable the chosen layer only after the expected read, write and purge behavior is demonstrated. If a check fails, revert the relevant cache change and inspect the request path, response headers and application behavior.

Document the public allowlist, sensitive routes, variant keys, freshness limits and invalidation events so future site changes can update the policy. Recheck when adding payment gateways, currency controls, localization or headless/API features. Correctness depends on keeping these rules aligned with the application over time.

Frequently Asked Questions

It can break flows or expose personal responses if eligibility and exclusions are wrong. Keep actual cart/checkout/account, session, authorization, action and API requests out of both shared-cache reads and writes. Start with a narrow public allowlist and test separate visitors/accounts; no cache product guarantees compatibility with every extension.

Compare the existing stack, cache-engine availability, operating responsibility and invalidation integration. Nginx FastCGI fits an existing Nginx/PHP setup; Varnish adds an HTTP policy layer; LiteSpeed needs a compatible server/plugin stack. ESI needs supported products and integration. Choose using measured needs and tested user flows.

Use comparable GET requests and inspect both bodies and cache status at the relevant layer. An eligible cold request may MISS then HIT, while prewarmed or uncacheable responses behave differently. Test that cookies, authorization, private/no-store and Set-Cookie responses are not reused, and that sensitive requests do not pollute the public cache. HEAD alone is insufficient.
↑