From qa-cache-testing
Wraps CDN cache-purge testing patterns for Cloudflare (POST /zones/{zone_id}/purge_cache, single-file / everything / cache-tags / hostname / prefix), Fastly (POST purge-by-key / purge-all, surrogate-keys via Surrogate-Key header), and CloudFront (CreateInvalidation API + paths). Covers end-to-end test patterns (write origin → trigger purge → assert edge serves fresh), purge-propagation delay testing (typically 1-30s globally), surrogate-key + cache-tag patterns for group-purge, and Cache-Status header verification (cf-cache-status: HIT/MISS/BYPASS). Use when designing or auditing CDN cache-invalidation workflows.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-cache-testing:cdn-cache-purge-testsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
CDN cache-purge tests verify that the
CDN cache-purge tests verify that the write-origin-then-invalidate-edge sequence works end-to-end - the most-likely-broken cache integration in real deployments.
Per developers.cloudflare.com/cache/how-to/purge-cache/, Cloudflare offers five purge methods (Single-file, Everything, Cache-tags, Hostname, Prefix). Fastly's surrogate-key pattern and CloudFront's invalidation API offer similar shapes.
curl -X POST \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"files":["https://example.com/api/users/1"]}'
Response:
{ "success": true, "result": { "id": "...." } }
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
--data '{"tags":["user-1","posts-feed"]}'
The response headers from the origin must have included
Cache-Tag: user-1, posts-feed for this to work.
curl -X POST "https://api.fastly.com/service/${SERVICE_ID}/purge/${SURROGATE_KEY}" \
-H "Fastly-Key: ${FASTLY_API_TOKEN}"
Origin responses include Surrogate-Key: user-1 posts-feed (space-separated).
aws cloudfront create-invalidation \
--distribution-id ${DIST_ID} \
--paths "/api/users/1" "/api/users/1/*"
Cloud-side wait + propagation. Returns an InvalidationId; poll
status via get-invalidation.
The canonical purge test:
import requests, time
def test_write_then_purge_serves_fresh():
# 1. Origin write
origin_response = requests.post(
"https://origin.example.com/api/users/1",
json={"name": "Alice"},
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
assert origin_response.status_code == 200
# 2. Verify edge has the *old* value (might or might not — cache hit)
edge_before = requests.get("https://example.com/api/users/1")
cache_status_before = edge_before.headers.get("cf-cache-status")
# 3. Trigger purge
purge = requests.post(
f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache",
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
json={"files": ["https://example.com/api/users/1"]},
)
assert purge.json()["success"]
# 4. Wait for global propagation (Cloudflare typically <30s)
time.sleep(10)
# 5. Verify edge fetches fresh from origin
edge_after = requests.get("https://example.com/api/users/1")
cache_status_after = edge_after.headers.get("cf-cache-status")
# Either MISS (just fetched) or HIT (re-cached fresh value)
assert cache_status_after in ("MISS", "HIT", "EXPIRED")
assert edge_after.json()["name"] == "Alice"
| Header | CDN | Common values |
|---|---|---|
cf-cache-status | Cloudflare | HIT, MISS, EXPIRED, BYPASS, DYNAMIC, REVALIDATED |
x-cache | Fastly | HIT, MISS, HIT-CLUSTER, HIT-CLUSTER-WAIT |
x-cache | CloudFront | Hit from cloudfront, Miss from cloudfront |
age | RFC 9111 standard | Seconds since cached |
EDGES = [
"https://example.com", # default
"https://eu.example.com", # geo-routed
"https://ap.example.com",
]
def test_purge_propagates_globally():
# Pre-cache in each region
for url in EDGES:
requests.get(url + "/api/users/1")
# Trigger purge
purge_url(API, "/api/users/1")
# Wait for global propagation
time.sleep(30)
# Verify each region serves fresh
for url in EDGES:
r = requests.get(url + "/api/users/1")
assert r.json()["name"] == "Alice"
| Field | Use |
|---|---|
cf-cache-status: HIT | Served from edge cache |
cf-cache-status: MISS | Origin pull just happened |
cf-cache-status: EXPIRED | Stale; revalidated from origin |
cf-cache-status: BYPASS | Cache deliberately skipped (e.g., uncacheable response) |
cf-cache-status: DYNAMIC | Not cached at all |
age: N | Per RFC 9111: seconds since cached |
For tests: assert on the transition (HIT → MISS after purge), not on the absolute state.
jobs:
cdn-purge-smoke:
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Write to origin
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
run: ./scripts/write-test-fixture.sh
- name: Purge edge
env:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
run: ./scripts/purge-test-paths.sh
- name: Verify edge serves fresh
run: pytest tests/cdn/test_purge_propagation.py
| Anti-pattern | Why it fails | Fix |
|---|---|---|
time.sleep(60) between purge + assertion | Slow; sometimes shorter / sometimes longer | Poll cf-cache-status until MISS (timeout 30s) |
| Test purge against prod | Pollutes prod cache; rate-limited | Dedicated test domain + zone |
| Purge-everything for smoke test | Massive cache flush; alarming for ops | Single-file or tag-based |
| Don't test multi-region | Edge in test region; user in another sees stale | Verify across regions |
No Cache-Tag / Surrogate-Key on origin | Group-purge has nothing to target | Origin must set tags |
| Use only cf-cache-status to verify fresh | Could be HIT of newly-purged fresh fetch | Compare response body to known fresh state |
| Skip purge-key naming review | Hot keys (all, feed) become noisy | Per-resource tagging strategy |
| Assume purge is synchronous | Cloudflare: <30s; CloudFront: minutes | Plan for async; poll |
browser-cache-control-tests.cache-coherence-patterns-reference,
stale-while-revalidate-reference.redis-cache-tests,
varnish-test-vtc-syntax,
browser-cache-control-tests.npx claudepluginhub testland/qa --plugin qa-cache-testingProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.