Use when CDN or browser caching needs tests: purges and invalidations on Cloudflare, Fastly (surrogate keys) and CloudFront, end-to-end write, purge and fresh-read checks, propagation delay and cache-status headers, plus browser Cache-Control, ETag and service-worker caching with Playwright.
83%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is testland/cdn-cache-purge-tests
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"The purge test proves the edge refreshes; the client tier can still serve
stale from the browser's own cache. Browser-side verification - asserting
the browser acts on Cache-Control as intended (served-from-cache via
CDP, ETag If-None-Match → 304 round-trips, Workbox service-worker
strategies, normal-vs-hard reload semantics) with Playwright across the
Chromium / Firefox / WebKit matrix - is in
references/browser-cache-control.md,
with deeper recipes in
references/playwright-cache-recipes.md.
Asserting only which header the server emits needs no browser - keep that
in the project's HTTP-level runner.
| 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 |
redis-cache-tests (its cache-coherence reference, incl. stampede + SWR/SIE in its
references/).redis-cache-tests,
varnish-test-vtc-syntax.testland/qa
aa16752
Canonical home
since Jul 23, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.