Server-Side Caching Explained: Speed Up Your Site for Users and Crawlers
Quick Summary
- What this covers: Implement server-side caching to reduce TTFB, improve Core Web Vitals, and optimize crawl budget. Technical guide covering Redis, Varnish, and CDN strategies.
- Who it's for: site owners and SEO practitioners
- Key takeaway: Read the first section for the core framework, then use the specific tactics that match your situation.
Server-side caching stores generated HTML, database query results, or computed data in fast-access memory layers, eliminating repetitive processing for identical requests. Instead of querying databases, executing PHP scripts, and rendering templates for every page load, cached versions serve instantly—reducing Time to First Byte (TTFB) from 800ms to 50ms and dramatically improving Core Web Vitals scores that influence rankings.
Google's crawler benefits equally from caching. Faster response times allow Googlebot to crawl more URLs per second within allocated crawl budget, discovering new content faster and refreshing existing pages more frequently. Sites with TTFB >600ms see reduced crawl rates; those under 200ms maximize crawl efficiency and indexing velocity.
Why Server-Side Caching Matters for SEO
Core Web Vitals impact: Largest Contentful Paint (LCP) begins when the server responds. Slow TTFB delays LCP, failing the 2.5-second threshold. Server-side caching reduces TTFB from 600-1200ms (typical dynamic generation) to 50-150ms (cached response), improving LCP by 500-1000ms.
Crawl budget optimization: Googlebot allocates finite crawl requests per site daily. Faster responses allow more pages crawled in the same timeframe. A site serving pages in 100ms instead of 800ms can be crawled 8x faster, enabling daily recrawls instead of weekly.
Reduced server load: Caching prevents server CPU and database saturation during traffic spikes. Servers handling 10,000 requests/hour without caching struggle at 1,000 simultaneous users hitting uncached pages. Cached responses serve at 100x lower computational cost.
Mobile performance: Mobile networks amplify latency. Caching reduces origin server round trips, minimizing the impact of slow 3G/4G connections. Users on 100ms latency connections see 200ms TTFB with caching vs. 900ms without.
Real-world measurement: Google Search Console's Core Web Vitals report flags "slow" TTFB as a contributing factor to poor LCP scores. PageSpeed Insights' "Reduce server response time" audit quantifies savings from caching implementation.
Types of Server-Side Caching
Page caching (full-page cache): Stores complete HTML output of rendered pages. Most effective for static or semi-static content (blog posts, product pages, landing pages).
Example flow without caching:
- User requests /blog/seo-guide
- Server executes PHP, queries database for post content, author details, comments
- Template engine renders HTML
- Server sends HTML to user
- Total time: 650ms
With page caching:
- User requests /blog/seo-guide
- Server checks cache for stored HTML
- Cache returns pre-rendered HTML
- Server sends HTML to user
- Total time: 45ms
Object caching: Stores database query results, API responses, or computed values in memory. Reduces database load without caching entire pages.
Example: WordPress stores post metadata, user sessions, and plugin data in object cache (Redis/Memcached), reducing database queries from 50/page to 5/page.
Opcode caching: Stores compiled PHP bytecode, eliminating script parsing overhead. PHP's OPcache extension provides this automatically in PHP 7.0+.
Database query caching: MySQL/PostgreSQL cache frequently executed queries. Limited effectiveness compared to application-level caching.
CDN edge caching: Content Delivery Networks cache static assets (images, CSS, JS) and HTML at geographically distributed servers. Reduces latency by serving content from locations nearest users.
Implementing Page Caching
Apache + mod_cache:
Enable modules:
a2enmod cache
a2enmod cache_disk
a2enmod headers
Configure caching in Apache config or .htaccess:
<IfModule mod_cache.c>
CacheEnable disk /
CacheRoot /var/cache/apache2/mod_cache_disk
CacheDefaultExpire 3600
CacheMaxExpire 86400
# Don't cache logged-in users
SetEnvIf Cookie "wordpress_logged_in" no-cache
CacheDisable /wp-admin
CacheDisable /cart
CacheDisable /checkout
</IfModule>
Nginx + FastCGI Cache:
Add to Nginx config:
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
server {
location ~ \.php$ {
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 60m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache-Status $upstream_cache_status;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
include fastcgi_params;
}
# Skip cache for dynamic pages
set $skip_cache 0;
if ($request_uri ~* "/wp-admin/|/cart/|/checkout/") {
set $skip_cache 1;
}
if ($http_cookie ~* "wordpress_logged_in") {
set $skip_cache 1;
}
}
WordPress caching plugins:
- WP Rocket: Commercial ($59/year), automatic page cache, minification, lazy loading
- W3 Total Cache: Free, page cache + object cache + browser cache + CDN integration
- WP Super Cache: Free, simpler than W3TC, generates static HTML files
- LiteSpeed Cache: Free, optimized for LiteSpeed servers, includes image optimization
All plugins write cached HTML to disk, serving via .htaccess rules before WordPress loads.
Varnish Cache:
Reverse proxy cache sitting in front of web servers. Extremely fast (serves 200,000+ requests/second).
Install Varnish:
apt install varnish
Configure VCL (Varnish Configuration Language) in /etc/varnish/default.vcl:
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
# Don't cache logged-in users
if (req.http.Cookie ~ "wordpress_logged_in") {
return (pass);
}
# Don't cache admin, cart, checkout
if (req.url ~ "^/wp-admin" || req.url ~ "^/cart" || req.url ~ "^/checkout") {
return (pass);
}
return (hash);
}
sub vcl_backend_response {
# Cache for 1 hour
set beresp.ttl = 1h;
# Serve stale content if backend is down
set beresp.grace = 6h;
}
Change Apache/Nginx to listen on port 8080, let Varnish handle port 80/443.
Object Caching with Redis
Redis is an in-memory key-value store ideal for caching database query results, session data, and transient data.
Install Redis:
apt install redis-server
systemctl enable redis-server
WordPress Redis integration:
Install Redis Object Cache plugin, configure wp-config.php:
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_DATABASE', 0);
WordPress automatically stores transients, user data, and post metadata in Redis instead of database.
Performance gain: Typical WordPress site reduces database queries from 40-60/page to 8-12/page, improving TTFB by 200-400ms.
Custom application Redis caching (PHP):
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Check cache
$cacheKey = 'product_' . $productId;
$product = $redis->get($cacheKey);
if (!$product) {
// Cache miss - query database
$product = $db->query("SELECT * FROM products WHERE id = ?", [$productId]);
// Store in cache for 1 hour
$redis->setex($cacheKey, 3600, json_encode($product));
} else {
// Cache hit - decode cached data
$product = json_decode($product);
}
Memcached alternative: Similar to Redis but simpler. No persistence, pure memory cache. Choose Redis for richer data structures and persistence; Memcached for pure speed and simplicity.
CDN Edge Caching
Content Delivery Networks cache content at edge locations worldwide, reducing latency by serving from nearest geographic server.
Cloudflare setup:
Sign up at cloudflare.com
Add domain, change nameservers
Enable caching via Page Rules:
- Cache Level: Standard (caches static resources)
- Browser Cache TTL: 1 month
- Edge Cache TTL: 2 hours
Configure cache-everything for specific URLs:
Page Rule: example.com/blog/*
Cache Level: Cache Everything
Edge Cache TTL: 2 hours
Cache headers: Control CDN caching via HTTP headers:
// Cache for 1 hour at CDN, 30 minutes in browser
header('Cache-Control: public, max-age=1800, s-maxage=3600');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 1800) . ' GMT');
- max-age: Browser cache duration
- s-maxage: CDN/proxy cache duration
- public: Allow caching by CDN and browsers
- private: Only browser caching, no CDN
- no-cache: Always revalidate with origin
- no-store: Don't cache at all
Cache purging: Invalidate cached content when updates occur:
Cloudflare API:
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"files":["https://example.com/blog/updated-post"]}'
KeyCDN:
curl -X DELETE "https://api.keycdn.com/zones/purgeurl/ZONE_ID.json" \
-u "API_KEY:" \
-d "urls=https://example.com/blog/updated-post"
Cache Invalidation Strategies
Time-based expiration (TTL): Cache expires after fixed duration (1 hour, 24 hours). Simple but may serve stale content.
fastcgi_cache_valid 200 1h; # Cache successful responses for 1 hour
Manual purge: Clear cache when content updates.
Nginx FastCGI purge:
rm -rf /var/cache/nginx/*
nginx -s reload
Varnish purge:
varnishadm "ban req.url ~ ^/blog/updated-post"
Smart invalidation: Purge specific cache keys when related content changes.
WordPress example: When post is updated, purge:
- Post page cache
- Homepage cache
- Category archive cache
- Author archive cache
- Related posts cache
WP Rocket automatic purge triggers:
- Post/page publish/update
- Comment approval
- Theme/plugin change
- Widget update
Cache warming: Rebuild cache proactively after purges. Crawler script requests important URLs to repopulate cache before users arrive.
#!/bin/bash
# cache-warmer.sh
urls=(
"https://example.com/"
"https://example.com/products/"
"https://example.com/blog/"
"https://example.com/about/"
)
for url in "${urls[@]}"; do
curl -s -o /dev/null "$url"
echo "Warmed: $url"
done
Run after cache purges to prevent cold-cache slowdowns.
Excluding Dynamic Content from Caching
Never cache:
- User account pages (/account/, /dashboard/)
- Shopping carts and checkout flows
- Admin panels (/wp-admin/, /admin/)
- Form submissions (POST requests)
- Logged-in user sessions
- Search result pages (unique per query)
- Pages with personalized content
Cookie-based exclusions:
set $skip_cache 0;
# Don't cache logged-in users
if ($http_cookie ~* "wordpress_logged_in|woocommerce_items_in_cart") {
set $skip_cache 1;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
URL-based exclusions:
# Don't cache admin or checkout
SetEnvIf Request_URI "^/wp-admin/" no-cache
SetEnvIf Request_URI "^/checkout/" no-cache
CacheDisable env=no-cache
Query parameter exclusions:
# Don't cache URLs with query parameters (unless whitelisted)
if ($args ~* ".*") {
set $skip_cache 1;
}
# But allow caching for pagination
if ($args = "page=2") {
set $skip_cache 0;
}
Monitoring Cache Performance
Cache hit ratio: Percentage of requests served from cache vs. origin.
Formula:
Hit Ratio = (Cache Hits / Total Requests) × 100
Target: 80-95% for static content sites, 60-80% for dynamic e-commerce.
Check Nginx cache status:
Add header to responses:
add_header X-Cache-Status $upstream_cache_status;
Responses show:
- HIT: Served from cache
- MISS: Not in cache, fetched from origin
- BYPASS: Intentionally skipped cache (logged-in user, POST request)
- EXPIRED: Cache entry expired, revalidating
Monitor via logs:
awk '/HIT/ {hits++} /MISS/ {misses++} END {print "Hit ratio:", hits/(hits+misses)*100"%"}' /var/log/nginx/access.log
Redis cache monitoring:
redis-cli info stats | grep keyspace
Shows cache hit/miss rates.
CDN analytics: Cloudflare, Fastly, and KeyCDN dashboards display:
- Cache hit ratio per URL
- Bandwidth saved via caching
- Cache purge history
- Geographic cache distribution
TTFB measurement:
Before caching:
curl -w "TTFB: %{time_starttransfer}\n" -o /dev/null -s https://example.com/
After caching:
curl -w "TTFB: %{time_starttransfer}\n" -o /dev/null -s https://example.com/
Expect 60-80% TTFB reduction.
Testing Cache Implementation
Verify cache headers:
curl -I https://example.com/
Look for:
Cache-Control: max-age=3600, public
Age: 1234
X-Cache: HIT
Age header: Shows how long content has been cached (seconds).
Test multiple requests:
for i in {1..5}; do
curl -w "TTFB: %{time_starttransfer}\n" -o /dev/null -s https://example.com/blog/post
done
First request (cache MISS): 600ms Subsequent requests (cache HIT): 50ms
Test cache bypass:
# Request with cache-busting parameter
curl -I "https://example.com/?nocache=1"
# Should show BYPASS or MISS
Test logged-in exclusion:
curl -I -H "Cookie: wordpress_logged_in=user123" https://example.com/
Should show cache BYPASS, confirming logged-in users get fresh content.
WebPageTest.org: Run before/after cache implementation tests. Compare TTFB and LCP metrics.
Common Caching Pitfalls
Caching user-specific content: Serving User A's personalized dashboard to User B.
Fix: Exclude user-specific URLs and cookie-based sessions from caching.
Stale content persisting: Updated content doesn't appear for hours.
Fix: Implement cache purge on content updates. Reduce TTL for frequently changing pages.
Cache poisoning: Malicious requests cache poisoned responses served to all users.
Fix: Sanitize cache keys, exclude unusual query parameters, implement security headers.
Overloading origin during cache miss storms: Traffic spike causes cache expiration, all requests hit origin simultaneously.
Fix: Implement "grace period" serving stale content while revalidating:
sub vcl_backend_response {
set beresp.grace = 6h; # Serve stale content up to 6 hours if origin is down
}
Caching errors: 404 or 500 responses cached, serving errors repeatedly.
Fix: Only cache successful responses:
fastcgi_cache_valid 200 1h; # Only cache HTTP 200
fastcgi_cache_valid 404 1m; # Cache 404s briefly to prevent repeated origin checks
fastcgi_cache_valid 500 0; # Never cache server errors
Frequently Asked Questions
Does caching affect SEO negatively?
No. Faster sites rank better. Google's crawler benefits from caching just like users. Ensure cached content matches what users see (no cloaking).
Should I cache mobile and desktop differently?
Not necessary with responsive design. With dynamic serving, cache separate mobile/desktop versions using Vary: User-Agent header.
How often should I purge cache?
On-demand when content updates. Scheduled purges (e.g., midnight daily) ensure daily content like news/blogs stays fresh.
Can I cache e-commerce product pages?
Yes, but exclude personalized elements (recently viewed, cart status) via AJAX. Cache product details, purge on price/inventory changes.
Does CloudFlare cache HTML by default?
Only static assets (images, CSS, JS). HTML requires "Cache Everything" page rule explicitly enabled.
What TTL should I use?
Static pages (blog posts): 24 hours Semi-static (product pages): 1-4 hours Dynamic (homepage, listings): 15-60 minutes Never cache: user accounts, checkout, admin
How do I cache WordPress pages for logged-in users?
Don't cache personalized content. Use fragment caching (cache static widgets separately) or AJAX load dynamic elements on cached pages.
Will caching break real-time features?
Yes. Exclude real-time pages (chat, notifications, stock tickers) from caching or implement WebSocket updates on cached pages.
When This Fix Isn't Your Priority
Skip this for now if:
- Your site has fundamental crawling/indexing issues. Fixing a meta description is pointless if Google can't reach the page. Resolve access, robots.txt, and crawl errors before optimizing on-page elements.
- You're mid-migration. During platform or domain migrations, freeze non-critical changes. The migration itself introduces enough variables — layer optimizations after the new environment stabilizes.
- The page gets zero impressions in Search Console. If Google shows no data for the page, the issue is likely discoverability or indexation, not on-page optimization. Investigate why the page isn't indexed first.