yediyuz/laravel-cloudflare-cache
Integrate Cloudflare edge caching into Laravel: cache responses, automatically purge URLs or tags on model changes, and manage cache behavior from familiar Laravel APIs. Reduce origin load and speed up responses with minimal configuration.
composer require yediyuz/laravel-cloudflare-cache
php artisan vendor:publish --tag="cloudflare-cache-config"
.env:
CLOUDFLARE_CACHE_API_TOKEN=...
CLOUDFLARE_CACHE_ZONE_ID=...
CLOUDFLARE_CACHE_ENABLED=true
The API token needs Zone → Cache Purge permission (and Zone → Zone Settings if you plan to toggle development mode). Never use a Global API Key — use a scoped token.config/cloudflare-cache.php — TTLs, tag prefix, bypass rules, environment gating.CloudflareCache in the HTTP kernel or aliased) — this is what actually writes Cache-Control / CDN-Cache-Control headers to responses.CloudflareCache::purge(), ::purgeTags()) — used from observers, jobs, and deploy scripts.Route::get('/pricing', PricingController::class)
->middleware('cloudflare-cache:3600');
Hit it once to warm the edge, then verify with:
curl -I https://example.com/pricing | grep -i -E 'cf-cache|age|cache-control'
You want cf-cache-status: HIT and/or an increasing age.The mental model: the package does not cache anything itself. It shapes the response headers Cloudflare reads, and it calls the Cloudflare purge API. Your Laravel cache driver is irrelevant to the edge cache.
Route-scoped middleware with explicit TTLs. Don’t apply caching globally. Put the middleware on the routes that are genuinely public and static: landing pages, product listings, category indexes, sitemaps. TTLs of 300–3600s are typical; anything that changes faster should be purged rather than short-TTL’d.
Two headers, two purposes. Treat Cache-Control as the browser TTL and CDN-Cache-Control (or Cloudflare-CDN-Cache-Control) as the edge-only TTL. A common setup is short browser cache, long edge cache, and purge-driven invalidation at the edge:
$response->headers->set('Cache-Control', 'public, max-age=60');
$response->headers->set('CDN-Cache-Control', 'public, max-age=86400');
Tag on write, purge on update. Add a stable tag per entity, then purge the tag inside an observer or model event:
// Response
$response->headers->set('Cache-Tag', implode(',', ["product-{$product->id}", 'products']));
// Observer
class ProductObserver {
public function saved(Product $product): void {
CloudflareCache::purgeTags(["product-{$product->id}", 'products']);
}
}
Keep a small allowlist of tags (products, home, sitemap) so purges stay predictable.
Purge on deploy, not on every request. Add a step to your deploy pipeline (Envoyer, Deployer, GitHub Actions) that purges the full zone or a set of high-level tags. This is the single highest-value use of the package day-to-day: stale edge content after a release is the most common complaint.
Bypass cache for authenticated or personalized requests. Either omit the middleware, or use a middleware ordering where your auth middleware forces private, no-store. Cloudflare will not cache responses with Set-Cookie or Cache-Control: private, but be explicit rather than relying on that.
Queue purges. Wrap purge calls in a job dispatched after commit (->afterCommit() or DB::afterCommit()), so you don’t purge for a transaction that rolls back, and so a purge-API timeout doesn’t fail the user request.
Use it with Laravel’s own cache, not instead of it. The edge cache does not replace Cache::remember() for origin-side load reduction. Layer: Laravel cache → Cloudflare edge → purge hooks.
Cache-Tag response header is widely supported, but the purge-by-tag API is an Enterprise-plan feature in Cloudflare. On Free/Pro/Business, plan on single-URL purges (purgeUrls([...])) or purgeEverything(). Check your plan before building a tag-based architecture around this package.purgeEverything has cooldowns). Batch URLs into a single call rather than looping. A purgeEverything() in a tight loop will burn your quota and return 429s that the package may or may not surface clearly.Vary and cookies silently kill caching. If your app sets a session cookie, CSRF cookie, or Vary: Cookie on a response you thought was cacheable, Cloudflare will bypass it and you’ll see cf-cache-status: DYNAMIC or BYPASS. Inspect response headers with curl -I on a fresh session.GET and HEAD are cached. POST/PUT/DELETE go straight to origin. This matters for API routes that you might be tempted to wrap.CLOUDFLARE_CACHE_ENABLED=false locally/CI so a saved() event in a test suite doesn’t purge your production zone. Guard with app()->environment('production') in the config or in the purge wrapper.curl -I and inspect cf-cache-status, age, cache-control, cdn-cache-control, cache-tag; (2) confirm the zone is not in Development Mode (which forces BYPASS); (3) confirm the DNS record is proxied (orange cloud) — an unproxied record means Cloudflare never sees the request; (4) confirm the URL has no cache-busting query string.Http::fake()) rather than the facade, so you actually assert the purge request payload and URL. Assert that observers dispatch the purge job on commit, and assert the job does not dispatch when the model is unchanged (isDirty()).How can I help you explore Laravel packages today?