Cloudflare Cache Setup for Feedframer API
This guide explains how to configure Cloudflare to cache your API responses for 1 hour, reducing server load while ensuring users only see their own data.
Overview
Cloudflare will cache API responses at the edge, serving them from nearby data centers instead of hitting your origin server. This dramatically reduces:
- Server CPU/memory usage
- Database queries
- API response times (from ~200ms to ~20ms)
Important Security Note
⚠️ Critical: You must configure the cache key to include the api_key parameter. Otherwise, Cloudflare will serve User A's cached data to User B, creating a major data leak.
Step 1: Laravel Cache Headers (Already Done ✅)
Your PostController now sends proper cache headers:
->header('Cache-Control', 'public, max-age=3600') // 1 hour cache
->header('Vary', 'Authorization') // Vary by auth
These headers tell Cloudflare:
public: Response can be cachedmax-age=3600: Cache for 1 hour (3600 seconds)Vary: Authorization: Different auth = different cache
Step 2: Cloudflare Dashboard Setup
A. Enable Caching for API Routes
- Log into Cloudflare Dashboard
- Select your domain
- Go to Caching → Configuration
- Under Caching Level, select Standard
B. Create Cache Rule for API Endpoints
- Go to Rules → Page Rules (or Cache Rules if available)
- Click Create Page Rule
- Configure:
URL Pattern: feedframer.com/api/v1/*
Settings:
✓ Cache Level: Cache Everything
✓ Edge Cache TTL: 1 hour
✓ Browser Cache TTL: 1 hour
✓ Origin Cache Control: Enabled (respect Laravel headers)
Using New Cache Rules (Recommended if available):
If your Cloudflare plan has the new "Cache Rules" feature:
- Go to Caching → Cache Rules
- Click Create Rule
- Configure:
Rule Name: API Cache - Posts Endpoint
If:
URI Path starts with /api/v1/
Then:
Eligible for cache: Yes
Edge TTL: Use cache-control header if present, otherwise 1 hour
Browser TTL: Respect origin
C. Configure Cache Key (CRITICAL FOR SECURITY)
This is the most important step to prevent data leaks.
- Go to Caching → Configuration
- Scroll to Cache Key section
- Click Create Custom Cache Key (Enterprise) or use Transform Rules (Business+)
For Enterprise Plans:
Custom Cache Key Template:
${scheme}://${host}${uri}${query:api_key}${query:filter.type}${query:filter.account}${query:page.cursor}${query:page.size}${query:sort}
For Business/Pro Plans (using Transform Rules):
- Go to Rules → Transform Rules → Modify Request Header
- Create rule:
Rule Name: API Cache Key
If:
URI Path starts with /api/v1/
Then:
Set dynamic header:
Header name: X-Cache-Key
Value: concat(http.request.uri.path, http.request.uri.query.api_key)
For Free Plans:
Cloudflare Free plans include query strings in cache keys by default, which is good. However, ensure you:
- Go to Caching → Configuration
- Under Query String Sort, enable it (ensures
?a=1&b=2and?b=2&a=1are cached the same) - Cloudflare will automatically include
api_keyin the cache key
⚠️ Verify: Test with two different API keys to ensure you get different responses.
Step 3: Verify Caching is Working
Test 1: Check Cache Headers
curl -I "https://feedframer.com/api/v1/me?api_key=YOUR_KEY"
Look for these headers:
CF-Cache-Status: HIT # Second request (cache hit)
CF-Cache-Status: MISS # First request (cache miss)
CF-Cache-Status: DYNAMIC # Not cached (check your rules)
CF-Cache-Status: EXPIRED # Cache expired, fetching fresh
Cache-Control: public, max-age=3600
Age: 234 # How long it's been cached (seconds)
Test 2: Verify Different API Keys = Different Cache
# User A's request
curl "https://feedframer.com/api/v1/me?api_key=USER_A_KEY"
# User B's request (should NOT return User A's data)
curl "https://feedframer.com/api/v1/me?api_key=USER_B_KEY"
These MUST return different data. If they don't, your cache key is not including api_key.
Test 3: Performance Check
# First request (cache miss - hits origin)
time curl "https://feedframer.com/api/v1/me?api_key=YOUR_KEY"
# Expected: ~200ms
# Second request (cache hit - from edge)
time curl "https://feedframer.com/api/v1/me?api_key=YOUR_KEY"
# Expected: ~20ms (10x faster!)
Step 4: Cache Purging Strategy
When posts are updated, you need to invalidate the cache.
Option A: Purge Everything (Simple)
Add to app/Jobs/FetchInstagramPosts.php after fetching posts:
use Illuminate\Support\Facades\Http;
// After successfully fetching posts
if (config('services.cloudflare.enabled')) {
$this->purgeCloudflareCache();
}
protected function purgeCloudflareCache(): void
{
Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.cloudflare.api_token'),
])->post('https://api.cloudflare.com/client/v4/zones/' . config('services.cloudflare.zone_id') . '/purge_cache', [
'purge_everything' => true,
]);
}
Option B: Purge by URL (Targeted - Better)
protected function purgeCloudflareCache(): void
{
$urls = [
config('app.url') . '/api/v1/me?api_key=' . $this->account->api_token,
];
Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.cloudflare.api_token'),
])->post('https://api.cloudflare.com/client/v4/zones/' . config('services.cloudflare.zone_id') . '/purge_cache', [
'files' => $urls,
]);
}
Add Cloudflare Config
Update .env:
CLOUDFLARE_ZONE_ID=your_zone_id_here
CLOUDFLARE_API_TOKEN=your_api_token_here
CLOUDFLARE_CACHE_ENABLED=true
Create config/services.php entry:
'cloudflare' => [
'zone_id' => env('CLOUDFLARE_ZONE_ID'),
'api_token' => env('CLOUDFLARE_API_TOKEN'),
'enabled' => env('CLOUDFLARE_CACHE_ENABLED', false),
],
Step 5: Monitor Cache Performance
Cloudflare Analytics
- Go to Analytics & Logs → Performance
- Check:
- Cache Hit Ratio: Should be >80% after warm-up
- Bandwidth Saved: Shows how much traffic served from cache
- Origin Requests: Should decrease significantly
Expected Metrics (After 24 Hours)
- Cache Hit Rate: 80-95%
- Origin Requests: Reduced by 80-95%
- Response Time: <50ms (vs ~200ms uncached)
- Bandwidth Savings: 70-90%
Troubleshooting
Issue: CF-Cache-Status is always DYNAMIC
Cause: Cache rule not matching or content type excluded
Fix:
- Verify Page Rule URL pattern matches exactly
- Check Cloudflare doesn't exclude
application/vnd.api+jsonfrom caching - Add Page Rule: Cache Level → Cache Everything
Issue: Different users seeing same data
Cause: Cache key doesn't include api_key
Fix:
- Verify query string caching is enabled
- On Enterprise, configure custom cache key
- Test with
curlusing different API keys
Issue: Cache never expires, showing stale data
Cause: Cache TTL too long or not respecting origin headers
Fix:
- Check
Cache-Controlheader is being sent from Laravel - Ensure Cloudflare is set to "Respect Origin Headers"
- Manually purge cache to verify
Issue: Cache hit rate is low (<50%)
Cause: Users using different query parameters
Fix:
- Enable "Query String Sort" in Cloudflare
- Normalize query parameters in Laravel before responding
- Consider limiting allowed query combinations
Advanced: Cache Warming
To pre-fill Cloudflare's cache after deploying:
// artisan command: app/Console/Commands/WarmCache.php
public function handle()
{
$apiKeys = User::pluck('api_token');
foreach ($apiKeys as $apiKey) {
Http::get(config('app.url') . '/api/v1/me?api_key=' . $apiKey);
}
}
Security Checklist
✅ Before Going Live:
- Verify
api_keyis included in cache key - Test with 2+ different API keys, confirm different responses
- Confirm
CF-Cache-Status: HITappears on second request - Check no sensitive headers leak (Authorization, Set-Cookie)
- Monitor for a week to ensure no cross-user data leaks
- Set up cache purging on post updates
- Configure rate limiting (already done in Laravel)
Expected Results
After proper configuration:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Response Time | 200ms | 20ms | 10x faster |
| Server Load | 100% | 5-10% | 90-95% reduction |
| Database Queries | 1000/min | 50/min | 95% reduction |
| Monthly Bandwidth | 100GB | 10GB | 90% savings |
| Concurrent Users | 100 | 1000+ | 10x capacity |