
Table of Contents
Put WordPress behind a CDN by caching anonymous HTML and static assets at the edge while bypassing admin, AJAX, REST and cart endpoints; keep logged-in users uncached and pass the true visitor IP to origin. You know it worked when curl -I shows x-cache: HIT for guests, BYPASS for logged-in cookies, /wp-admin/ never caches, and wp-config.php logs the real client address, not the CDN node.
Preparation Before You Touch DNS
Inventory your stack: note PHP version (8.3 is baseline in 2026), WordPress 6.7 or newer, active cache plugin, and your CDN vendor endpoint. Confirm you have SSH and wp CLI access. Take a full backup and record current DNS TTL — lower it to 300 seconds 24 hours before cutover so rollback is fast. Verify your origin serves correct Host headers; many misconfigured CDNs break because the origin only answers the bare domain, so test with curl -H 'Host: example.com' http://origin-ip/ before pointing traffic. If you are unsure about your stack hygiene, review our WordPress Pre-Launch Checklist: 26 Things to Verify so the origin is clean before edge logic is layered on top.
Paths That Must Not Be Cached
CDN rules should bypass anything stateful or user-specific. Cache these never: /wp-admin/*, /wp-login.php, /wp-cron.php, /xmlrpc.php, /wp-json/* (unless you deliberately cache public REST routes), /cart/*, /checkout/*, /my-account/* for WooCommerce, any endpoint receiving POST, and preview URLs containing preview=true. Also bypass when cookies named wordpress_logged_in_*, wp-postpass_*, or comment_author_* are present, and when HTTP_X_REQUESTED_WITH equals XMLHttpRequest.
Concrete CDN rule set
Express this as a request-condition rule in your CDN panel or Terraform: bypass cache if path matches regex ^/(wp-admin|wp-login.php|wp-cron.php|xmlrpc.php|cart|checkout|my-account) OR http.request.method == "POST" OR cookie regex wordpress_logged_in_|wp-postpass_|comment_author_. In Nginx acting as origin shield you mirror the intent:
location ~* ^/(wp-admin|wp-login.php|wp-cron.php|xmlrpc.php) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
proxy_no_cache 1;
proxy_cache_bypass 1;
proxy_pass http://php_upstream;
}
location /wp-json/ {
add_header Cache-Control "no-store";
proxy_no_cache 1;
proxy_cache_bypass 1;
proxy_pass http://php_upstream;
}
Verify with curl -sI https://example.com/wp-admin/ | grep -i cache-control; you want no-store. Pitfall: some page builders enqueue admin-ajax for public pages — if you bypass all admin-ajax.php you may break front-end filters, so allow admin-ajax.php only when the action parameter is public and cacheable, otherwise bypass. Rollback: disable the CDN rule or set the path rule to “cache everything” temporarily while you debug; keep the origin no-store header as your safety net.
HTML Cache vs Logged-In Users
Anonymous visitors get cached HTML; logged-in users must always hit origin. WordPress sets wordpress_logged_in_[hash] and wp-settings-[uid] cookies. Configure your CDN to serve from cache only when no auth cookie is present, and to vary cache key by those cookies so a stray cookie does not poison the guest cache. Do not rely on Cache-Control: public from WordPress alone — most installs send no-cache by default, so you override at the edge with a rule: “if no auth cookie, cache HTML for 600s and strip cookies from cache key”.
Edge logic example
Get a Quote
Email + requirement + budget range — we reply with a quote within 24 hours.
# pseudo edge rule (vendor-agnostic, 2026 syntax)
if (http.cookie !~ "wordpress_logged_in_|wp-postpass_|comment_author_") {
set cache_ttl = 600;
remove_from_key: cookie;
deliver_from_cache;
} else {
bypass_cache;
forward_all_cookies_to_origin;
}
Test with two requests. Guest: curl -sI https://example.com/ | grep -i x-cache returns HIT after warm-up. Then log in, copy the cookie string, and run curl -sI -H 'Cookie: wordpress_logged_in_abc=xyz' https://example.com/ | grep -i x-cache; you must see BYPASS or MISS. Pitfall: page-cache plugins like WP Rocket write their own HTML files and may set cookies for “user just posted a comment” — if you cache those you leak commenter state to guests. Rollback: set HTML TTL to 0 in the CDN panel or switch DNS back to origin; logged-in users keep working because origin never depended on the CDN.
Step-by-Step CDN Setup Process
- Lower DNS TTL to 300 and back up files plus database.
- Add the never-cache path and cookie rules from the previous section in your CDN dashboard.
- Point DNS (CNAME or A/AAAA via CDN anycast) to the CDN-provided edge hostname.
- Install TLS at the edge and set origin pull to speak HTTPS to your server.
- Configure real-IP restoration at origin using the CDN’s IP ranges and header.
- Warm the cache, purge on deploy, and verify HIT/BYPASS with curl.
Run each step in order and verify with the matching command before moving on. If you need a clean room to test these rules, our write-up on Setting Up a WordPress Staging Environment Safely shows how to mirror production without leaking data.
Busting Cache After CSS Changes
Static assets should be versioned so a deploy never serves stale CSS. In 2026 use file-modification versioning in your theme’s functions.php rather than a constant version string:
function my_theme_assets() {
$css_path = get_template_directory() . '/style.css';
$css_ver = filemtime( $css_path );
wp_enqueue_style(
'theme-style',
get_template_directory_uri() . '/style.css',
array(),
$css_ver
);
}
add_action( 'wp_enqueue_scripts', 'my_theme_assets' );
Every deploy changes filemtime, so the URL becomes style.css?ver=1712345678 and the CDN treats it as a new object. After deploy, run a targeted purge rather than nuking everything: wp cdn purge --path=/style.css* via your vendor’s WP-CLI add-on, or call the API directly:
curl -X POST "https://api.cdnvendor.com/v1/zones/ZONE_ID/purge"
-H 'Authorization: Bearer TOKEN'
-H 'Content-Type: application/json'
--data '{"files":["https://example.com/style.css?ver=*"]}'
Verify with curl -sI 'https://example.com/style.css?ver=1712345678' | grep -i x-cache; first request is MISS, second is HIT. Pitfall: if you use a cache-busting query string but the CDN is configured to “ignore query string”, the bust fails — either include query in cache key for assets or use hashed filenames like style.a1b2c3.css. Rollback: revert the enqueue version to the old constant and re-purge; the old asset URL starts resolving again.
Origin and Real Client IP

Behind a CDN, PHP sees the edge node IP in REMOTE_ADDR, which breaks rate limiting, security plugins and analytics. Restore the true client IP by trusting the CDN’s header and its IP ranges. Most vendors send CF-Connecting-IP, True-Client-IP, or X-Forwarded-For. In Nginx:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# add every documented CDN range for 2026
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
Reload with nginx -t && systemctl reload nginx. In WordPress, add to wp-config.php above the /* That's all */ line:
if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
}
Verify by creating a temporary MU-plugin that writes to debug.log: error_log( 'IP seen: ' . $_SERVER['REMOTE_ADDR'] ); then curl from your own machine and confirm the log shows your public address, not a CDN range. Pitfall: trusting X-Forwarded-For from any source lets attackers spoof IPs — only trust it from known CDN ranges, and never enable it on origins exposed directly to the internet. Rollback: comment out the wp-config.php block and remove the Nginx set_real_ip_from lines, then reload; origin falls back to node IPs which is safe if imperfect. For transport-level correctness ensure the edge-to-origin hop is encrypted as described in HTTPS for WordPress: Certificates, Mixed Content, Redirects.
Verification Matrix
| Check | Command | Pass condition |
|---|---|---|
| Guest HTML cached | curl -sI https://example.com/ | grep x-cache | HIT on 2nd request |
| Admin bypass | curl -sI https://example.com/wp-admin/ | grep cache-control | no-store |
| Logged-in bypass | curl -sI -H 'Cookie: wordpress_logged_in_=x' / | grep x-cache | BYPASS |
| Real IP | tail debug.log after curl | your public IP |
| Asset purge | curl -sI style.css?ver=new | MISS then HIT |
Run the matrix after every rule change. If any row fails, roll back just that rule rather than the whole CDN layer.
Action Checklist
- Bypass cache for admin, login, REST, AJAX, cart and POST paths.
- Serve cached HTML only to requests without auth cookies.
- Version assets with filemtime and purge specific URLs on deploy.
- Restore real client IP via trusted CDN ranges and headers.
- Keep DNS TTL low and origin no-store headers as rollback safety.
Need production-grade edge logic for your stack? See our Custom WordPress Development team for hands-on CDN and cache engineering.
Frequently asked questions
How do I know if my WordPress CDN setup is actually caching HTML?
Run curl -sI https://example.com/ twice from a clean IP. The first response shows x-cache: MISS, the second shows HIT. If both are BYPASS, your cookie or path rules are too aggressive.
Should I cache the WordPress REST API at the CDN?
Only cache public, non-authenticated REST routes with a short TTL and never cache /wp-json/wp/v2/users or endpoints behind nonces. Default to bypass for everything under /wp-json/.
What is the safest rollback if the CDN breaks the site?
Change DNS back to the origin A record (low TTL makes this minutes) or set the CDN rule to “bypass cache for all paths”. Because your origin already sends correct no-store for dynamic paths, the site works immediately.
