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 cached
  • max-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

  1. Log into Cloudflare Dashboard
  2. Select your domain
  3. Go to CachingConfiguration
  4. Under Caching Level, select Standard

B. Create Cache Rule for API Endpoints

  1. Go to RulesPage Rules (or Cache Rules if available)
  2. Click Create Page Rule
  3. 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:

  1. Go to CachingCache Rules
  2. Click Create Rule
  3. 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.

  1. Go to CachingConfiguration
  2. Scroll to Cache Key section
  3. 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):

  1. Go to RulesTransform RulesModify Request Header
  2. 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:

  1. Go to CachingConfiguration
  2. Under Query String Sort, enable it (ensures ?a=1&b=2 and ?b=2&a=1 are cached the same)
  3. Cloudflare will automatically include api_key in 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

  1. Go to Analytics & LogsPerformance
  2. 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:

  1. Verify Page Rule URL pattern matches exactly
  2. Check Cloudflare doesn't exclude application/vnd.api+json from caching
  3. Add Page Rule: Cache Level → Cache Everything

Issue: Different users seeing same data

Cause: Cache key doesn't include api_key

Fix:

  1. Verify query string caching is enabled
  2. On Enterprise, configure custom cache key
  3. Test with curl using different API keys

Issue: Cache never expires, showing stale data

Cause: Cache TTL too long or not respecting origin headers

Fix:

  1. Check Cache-Control header is being sent from Laravel
  2. Ensure Cloudflare is set to "Respect Origin Headers"
  3. Manually purge cache to verify

Issue: Cache hit rate is low (<50%)

Cause: Users using different query parameters

Fix:

  1. Enable "Query String Sort" in Cloudflare
  2. Normalize query parameters in Laravel before responding
  3. 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_key is included in cache key
  • Test with 2+ different API keys, confirm different responses
  • Confirm CF-Cache-Status: HIT appears 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

Additional Resources