HTTP Cache Debugging Tools
Practical tools and commands for inspecting HTTP cache headers and debugging Drupal caching behavior.
When to Use
-
Inspecting cache response headers (X-Drupal-Cache, Cache-Control, etc.)
-
Verifying cache hit/miss status
-
Debugging why pages aren't caching
-
Testing authenticated vs anonymous caching
-
Analyzing Vary headers and cache variations
SparkFabrik Project Context
For container access and service URLs in SparkFabrik projects, see the pkg-skills reference.
Quick reminder:
-
Inside container: http://drupal-nginx
-
From host: Use fs-cli pkg:get-urls to get external URL
curl - The Essential Tool
Basic Header Inspection
Get headers only (-I = HEAD request, -s = silent)
curl -sI https://example.com/
GET request with headers shown (-i includes headers in output)
curl -si https://example.com/
Follow redirects (-L)
curl -sIL https://example.com/
Cache-Specific Header Filters
Filter Drupal cache headers
curl -sI https://example.com/ | grep -iE 'x-drupal|cache-control|age|vary|etag'
Full cache header analysis
curl -sI https://example.com/ | grep -iE 'x-drupal|cache|age|vary|etag|expires|pragma|last-modified'
Just Drupal-specific headers
curl -sI https://example.com/ | grep -i 'x-drupal'
Authenticated Requests
With session cookie (simulate logged-in user)
curl -sI -H "Cookie: SESSabc123=xyz789" https://example.com/
With basic auth
curl -sI -u username:password https://example.com/
With bearer token
curl -sI -H "Authorization: Bearer TOKEN" https://example.com/
Cache Bypass Testing
Force cache miss with Cache-Control header
curl -sI -H "Cache-Control: no-cache" https://example.com/
Add random query string to bypass cache
curl -sI "https://example.com/?nocache=$(date +%s)"
Pragma no-cache (legacy)
curl -sI -H "Pragma: no-cache" https://example.com/
Timing and Performance
Show timing breakdown
curl -sI -w "\n\nTiming:\n DNS: %{time_namelookup}s\n Connect: %{time_connect}s\n TLS: %{time_appconnect}s\n TTFB: %{time_starttransfer}s\n Total: %{time_total}s\n" https://example.com/
Simple total time
curl -sI -w "\nTotal time: %{time_total}s\n" https://example.com/
Examples
Example 1: Quick Cache Status Check
Input: "Check if the homepage is being cached"
Output:
From SparkFabrik container
make drupal-cli
Inside container
curl -sI http://drupal-nginx/ | grep -iE 'x-drupal|cache-control'
Expected output for cached page:
X-Drupal-Cache: HIT
X-Drupal-Dynamic-Cache: HIT
Cache-Control: max-age=3600, public
Example 2: Compare Anonymous vs Authenticated
Input: "Check caching difference between anonymous and logged-in users"
Output:
Anonymous request
echo "=== Anonymous ===" curl -sI http://drupal-nginx/node/1 | grep -iE 'x-drupal|cache-control|set-cookie'
Authenticated request (with session cookie)
echo "=== Authenticated ===" curl -sI -H "Cookie: SESSxyz=abc123" http://drupal-nginx/node/1 | grep -iE 'x-drupal|cache-control'
Expected:
Anonymous: X-Drupal-Cache: HIT, Cache-Control: public
Authenticated: X-Drupal-Dynamic-Cache: HIT, Cache-Control: private
Example 3: Debug UNCACHEABLE Response
Input: "Find out why the page shows UNCACHEABLE"
Output:
Enable debug headers first (in settings.local.php):
$settings['http.response.debug_cacheability_headers'] = TRUE;
Then check all cache metadata
curl -sI http://drupal-nginx/node/1 | grep -iE 'x-drupal'
Look for:
X-Drupal-Cache-Max-Age: 0 → Something set max-age to 0
X-Drupal-Cache-Contexts: user → High-cardinality context
X-Drupal-Dynamic-Cache: UNCACHEABLE → Page cannot be cached
Example 4: Test Cache Invalidation
Input: "Verify cache clears after content edit"
Output:
Step 1: Check current cache status
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
X-Drupal-Cache: HIT
Step 2: Edit the node (via UI or drush)
drush php-eval "\Drupal::entityTypeManager()->getStorage('node')->load(1)->save();"
Step 3: Verify cache miss after invalidation
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
X-Drupal-Cache: MISS
Step 4: Confirm next request is cached
curl -sI http://drupal-nginx/node/1 | grep 'X-Drupal-Cache'
X-Drupal-Cache: HIT
Example 5: Check Vary Headers
Input: "See what the page varies on"
Output:
curl -sI http://drupal-nginx/ | grep -iE 'vary|x-drupal-cache-contexts'
Example output:
Vary: Cookie, Accept-Encoding
X-Drupal-Cache-Contexts: languages:language_interface theme url.path user.permissions
The Vary header tells proxies/CDNs what to cache separately
Cache-Contexts show Drupal's internal cache variations
Example 6: Full Debug Script
Input: "Give me a complete cache debug for a URL"
Output:
#!/bin/bash
cache-debug.sh - Run from inside drupal-tools container
URL="${1:-http://drupal-nginx/}"
echo "=== Cache Debug for: $URL ===" echo ""
echo "--- Response Headers ---" curl -sI "$URL" | grep -iE 'http/|x-drupal|cache|vary|age|etag|expires'
echo "" echo "--- Timing ---" curl -sI -w "DNS: %{time_namelookup}s | Connect: %{time_connect}s | TTFB: %{time_starttransfer}s | Total: %{time_total}s\n" -o /dev/null "$URL"
echo "" echo "--- Cache Status Summary ---" CACHE=$(curl -sI "$URL" | grep -i 'X-Drupal-Cache:' | cut -d' ' -f2 | tr -d '\r') DYNAMIC=$(curl -sI "$URL" | grep -i 'X-Drupal-Dynamic-Cache:' | cut -d' ' -f2 | tr -d '\r')
echo "Page Cache: ${CACHE:-N/A}" echo "Dynamic Cache: ${DYNAMIC:-N/A}"
if [ "$DYNAMIC" = "UNCACHEABLE" ]; then echo "⚠️ WARNING: Page is UNCACHEABLE - check for max-age:0" fi
Usage from SparkFabrik project
docker compose run --rm -it drupal-tools bash -c 'URL="http://drupal-nginx/node/1"; curl -sI "$URL" | grep -iE "x-drupal|cache"'
Example 7: Watch Cache Headers in Real-Time
Input: "Monitor cache status during development"
Output:
Watch cache status every 2 seconds
watch -n 2 'curl -sI http://drupal-nginx/ | grep -iE "x-drupal|cache-control"'
Or with timestamp
while true; do echo "=== $(date) ===" curl -sI http://drupal-nginx/ | grep -iE 'x-drupal' sleep 5 done
Example 8: Test Multiple URLs
Input: "Check cache status for multiple pages"
Output:
List of URLs to check
URLS=( "http://drupal-nginx/" "http://drupal-nginx/node/1" "http://drupal-nginx/admin" "http://drupal-nginx/user/login" )
for url in "${URLS[@]}"; do echo "=== $url ===" curl -sI "$url" | grep -iE 'x-drupal-cache|x-drupal-dynamic' || echo "No cache headers" echo "" done
Alternative to curl: httpie
httpie provides more readable syntax with colorized output.
Installation
If not present in the drupal-tools container:
Enter container as root
make drupal-cli-root
Install httpie
apk add --no-cache httpie
Usage
Headers only
http HEAD http://drupal-nginx/
With specific headers
http http://drupal-nginx/ 'Cookie:SESSxyz=abc'
Filter headers
http --print=h http://drupal-nginx/ | grep -i cache
Compare anonymous vs authenticated
http --print=h HEAD http://drupal-nginx/node/1 http --print=h HEAD http://drupal-nginx/node/1 'Cookie:SESSxyz=abc'
Browser DevTools
For visual debugging:
-
Network tab → Select request → Headers section
-
Filter by: cache or x-drupal
-
Disable cache: Network tab → Check "Disable cache"
-
Preserve log: Keep requests across navigation
DevTools Cache Headers to Check
Header Location Meaning
X-Drupal-Cache
Response Page Cache status
X-Drupal-Dynamic-Cache
Response Dynamic Cache status
Cache-Control
Response Browser/proxy caching rules
Age
Response Seconds since cached by proxy
Vary
Response What causes cache variations
Quick Reference
Task Command
Check cache status curl -sI URL | grep -i x-drupal
Full headers curl -sI URL
Authenticated curl -sI -H "Cookie: SESS=x" URL
Bypass cache curl -sI -H "Cache-Control: no-cache" URL
Timing curl -sI -w "TTFB: %{time_starttransfer}s\n" URL
Follow redirects curl -sIL URL
Anonymous vs Authenticated Cache Analysis
This section helps analyze how Drupal caches pages for anonymous and authenticated users.
Step-by-Step Analysis
- Get a Valid Session Cookie
First, log in to Drupal and extract the session cookie:
Option A: From browser DevTools
1. Log in to Drupal
2. Open DevTools → Application → Cookies
3. Copy the SESS* cookie value (e.g., SESSabc123=xyz789)
Option B: Via curl (if you have credentials)
curl -c cookies.txt -X POST
-d "name=admin&pass=password&form_id=user_login_form&op=Log+in"
http://drupal-nginx/user/login
Extract session cookie
cat cookies.txt | grep SESS
- Compare Anonymous vs Authenticated Headers
URL="http://drupal-nginx/node/1"
echo "========== ANONYMOUS REQUEST ==========" curl -sI "$URL" | grep -iE 'http/|x-drupal|cache-control|set-cookie|vary'
echo "" echo "========== AUTHENTICATED REQUEST ==========" curl -sI -H "Cookie: SESSxxxxxxx=yyyyyyyy" "$URL" | grep -iE 'http/|x-drupal|cache-control|vary'
- Interpret the Results
Key headers to analyze:
Header Anonymous (expected) Authenticated (expected) Meaning
X-Drupal-Cache
HIT or MISS
Not present Page Cache (only for anonymous)
X-Drupal-Dynamic-Cache
HIT
HIT or UNCACHEABLE
Dynamic Page Cache
Cache-Control
max-age=X, public
max-age=0, private, no-cache
Browser/proxy caching
Vary
Cookie, Accept-Encoding
Cookie, Accept-Encoding
Cache variations
Set-Cookie
May set session Should not set new session Session handling
Understanding Cache Behavior
Scenario 1: Optimal Caching (Anonymous)
X-Drupal-Cache: HIT X-Drupal-Dynamic-Cache: HIT Cache-Control: max-age=3600, public
✅ Good: Page is fully cached, served from Page Cache.
Scenario 2: Dynamic Cache Only (Anonymous)
X-Drupal-Cache: MISS X-Drupal-Dynamic-Cache: HIT Cache-Control: max-age=3600, public
⚠️ Partial: Page uses Dynamic Cache but not Page Cache. Check if there are session cookies being set.
Scenario 3: Uncacheable (Anonymous)
X-Drupal-Cache: MISS X-Drupal-Dynamic-Cache: UNCACHEABLE Cache-Control: must-revalidate, no-cache, private
❌ Problem: Page cannot be cached. Check for:
-
max-age: 0 on render elements
-
High-cardinality cache contexts (e.g., user )
-
Session being started unexpectedly
Scenario 4: Authenticated User (Expected)
X-Drupal-Dynamic-Cache: HIT Cache-Control: max-age=0, private, no-cache
✅ Expected: Authenticated pages should be private, Dynamic Cache can still help.
Scenario 5: Authenticated User Uncacheable
X-Drupal-Dynamic-Cache: UNCACHEABLE Cache-Control: must-revalidate, no-cache, private
⚠️ Check: Even for authenticated users, Dynamic Cache should work. Look for max-age: 0 issues.
Full Comparison Script
#!/bin/bash
cache-compare.sh - Compare anonymous vs authenticated caching
URL="${1:-http://drupal-nginx/}" SESSION_COOKIE="${2:-}"
echo "╔════════════════════════════════════════════════════════════════╗" echo "║ Cache Analysis: $URL" echo "╚════════════════════════════════════════════════════════════════╝" echo ""
echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ ANONYMOUS REQUEST │" echo "└─────────────────────────────────────────────────────────────────┘" ANON_HEADERS=$(curl -sI "$URL") echo "$ANON_HEADERS" | grep -iE 'http/|x-drupal|cache-control|vary|set-cookie'
ANON_PAGE_CACHE=$(echo "$ANON_HEADERS" | grep -i 'X-Drupal-Cache:' | awk '{print $2}' | tr -d '\r') ANON_DYN_CACHE=$(echo "$ANON_HEADERS" | grep -i 'X-Drupal-Dynamic-Cache:' | awk '{print $2}' | tr -d '\r') ANON_CACHE_CTRL=$(echo "$ANON_HEADERS" | grep -i 'Cache-Control:' | cut -d':' -f2 | tr -d '\r')
echo "" echo "Summary:" echo " Page Cache: ${ANON_PAGE_CACHE:-N/A}" echo " Dynamic Cache: ${ANON_DYN_CACHE:-N/A}" echo " Cache-Control: ${ANON_CACHE_CTRL:-N/A}"
if [ -n "$SESSION_COOKIE" ]; then echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ AUTHENTICATED REQUEST │" echo "└─────────────────────────────────────────────────────────────────┘" AUTH_HEADERS=$(curl -sI -H "Cookie: $SESSION_COOKIE" "$URL") echo "$AUTH_HEADERS" | grep -iE 'http/|x-drupal|cache-control|vary'
AUTH_DYN_CACHE=$(echo "$AUTH_HEADERS" | grep -i 'X-Drupal-Dynamic-Cache:' | awk '{print $2}' | tr -d '\r') AUTH_CACHE_CTRL=$(echo "$AUTH_HEADERS" | grep -i 'Cache-Control:' | cut -d':' -f2 | tr -d '\r')
echo "" echo "Summary:" echo " Dynamic Cache: ${AUTH_DYN_CACHE:-N/A}" echo " Cache-Control: ${AUTH_CACHE_CTRL:-N/A}" fi
echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ DIAGNOSIS │" echo "└─────────────────────────────────────────────────────────────────┘"
Anonymous diagnosis
if [ "$ANON_PAGE_CACHE" = "HIT" ]; then echo "✅ Anonymous: Page Cache is working" elif [ "$ANON_DYN_CACHE" = "HIT" ]; then echo "⚠️ Anonymous: Only Dynamic Cache working (Page Cache MISS)" elif [ "$ANON_DYN_CACHE" = "UNCACHEABLE" ]; then echo "❌ Anonymous: Page is UNCACHEABLE - needs investigation" else echo "⚠️ Anonymous: Cache status unclear" fi
Authenticated diagnosis
if [ -n "$SESSION_COOKIE" ]; then if [ "$AUTH_DYN_CACHE" = "HIT" ]; then echo "✅ Authenticated: Dynamic Cache is working" elif [ "$AUTH_DYN_CACHE" = "UNCACHEABLE" ]; then echo "⚠️ Authenticated: Dynamic Cache not working" fi
if echo "$AUTH_CACHE_CTRL" | grep -q "private"; then echo "✅ Authenticated: Correctly marked as private" else echo "❌ Authenticated: Should be private but isn't!" fi fi
Usage:
Anonymous only
./cache-compare.sh http://drupal-nginx/node/1
With authenticated comparison
./cache-compare.sh http://drupal-nginx/node/1 "SESSabc123=xyz789"
Common Issues and Solutions
Symptom Likely Cause Solution
Anonymous gets UNCACHEABLE
Something sets max-age: 0
Enable debug headers, check for bad cache metadata
Anonymous gets Set-Cookie
Session started for anonymous Check for code that calls \Drupal::currentUser() early
Anonymous Cache-Control: private
Session or user context Look for user cache context being added
Page Cache always MISS
Vary on Cookie + session exists Ensure anonymous users don't get sessions
Authenticated UNCACHEABLE
max-age: 0 in render array Find element setting zero max-age
Debug Headers
Enable detailed cache debug headers in settings.local.php :
$settings['http.response.debug_cacheability_headers'] = TRUE;
This exposes additional headers:
-
X-Drupal-Cache-Tags
-
Cache tags for invalidation
-
X-Drupal-Cache-Contexts
-
What the page varies on
-
X-Drupal-Cache-Max-Age
-
Minimum max-age from all elements
Container Quick Commands
SparkFabrik: Open interactive shell
make drupal-cli
SparkFabrik: One-off command
docker compose run --rm -it drupal-tools curl -sI http://drupal-nginx/
Generic Docker Compose
docker compose exec php curl -sI http://localhost/