• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Unlocking Next-Gen Performance: Advanced Caching Strategies for Laravel with Redis and Cloudflare Workers

Unlocking Next-Gen Performance: Advanced Caching Strategies for Laravel with Redis and Cloudflare Workers

Leveraging Redis for In-Memory Caching in Laravel

For applications demanding sub-millisecond response times for frequently accessed data, an in-memory cache is paramount. Redis, with its versatile data structures and high throughput, is an excellent choice. We’ll explore configuring Laravel to utilize Redis for both general cache operations and more specific use cases like query caching and rate limiting.

Configuring Laravel’s Cache Manager for Redis

The primary configuration file for cache settings in Laravel is config/cache.php. To use Redis, we need to define a Redis store. Ensure you have the predis/predis or phpredis extension installed and configured in your php.ini. For this example, we’ll assume predis/predis is installed.

First, update your .env file with your Redis connection details:

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Next, modify config/cache.php to include a Redis store definition. If you’re using Laravel’s default configuration, you’ll likely have a stores array. Add or modify the ‘redis’ entry:

<?php

return [
    // ... other configurations

    'stores' => [
        // ... other stores

        'redis' => [
            'driver' => 'redis',
            'connection' => 'cache_redis', // This refers to a connection defined in config/database.php
        ],

        // ... other stores
    ],

    // ... other configurations
];
</php>

Now, ensure your config/database.php has a corresponding Redis connection named cache_redis. If you’re using the default Redis setup for sessions and queues, you might already have this. If not, add it:

<?php

return [
    // ... other configurations

    'redis' => [
        'client' => env('REDIS_CLIENT', 'predis'), // or 'phpredis'

        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'redis'),
            'parameters' => [
                'password' => env('REDIS_PASSWORD'),
                'port' => env('REDIS_PORT', 6379),
                'scheme' => env('REDIS_SCHEME', 'tcp'),
                'host' => env('REDIS_HOST', '127.0.0.1'),
            ],
        ],

        'default' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => env('REDIS_DB', 0),
        ],

        // This is the connection referenced in config/cache.php
        'cache_redis' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => env('REDIS_CACHE_DB', 1), // Use a different DB for cache if desired
        ],
    ],

    // ... other configurations
];
</php>

Finally, set the default cache driver in config/cache.php to ‘redis’:

<?php

return [
    // ... other configurations

    'default' => env('CACHE_DRIVER', 'redis'), // Set to 'redis'

    // ... other configurations
];
</php>

Implementing Advanced Caching Patterns

With Redis configured, we can implement sophisticated caching strategies. A common pattern is caching expensive query results.

Caching Eloquent Queries

Instead of directly executing a query every time, we can cache its results. This is particularly useful for read-heavy endpoints that fetch static or slowly changing data.

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class ProductController extends Controller
{
    public function index()
    {
        // Define a unique cache key. Using a hash of query parameters can be robust.
        $cacheKey = 'products.all.' . md5(json_encode(request()->all()));
        $cacheDuration = 60 * 60; // Cache for 1 hour

        // Attempt to retrieve data from cache
        $products = Cache::remember($cacheKey, $cacheDuration, function () {
            // If not in cache, execute the query and return the result
            // Use 'get' to avoid N+1 query issues if relationships are eager loaded
            return Product::with('category')->get()->map(function ($product) {
                // Transform data if necessary before caching
                return [
                    'id' => $product->id,
                    'name' => $product->name,
                    'price' => $product->price,
                    'category_name' => $product->category->name ?? 'Uncategorized',
                ];
            });
        });

        return response()->json($products);
    }

    public function show($id)
    {
        $cacheKey = 'product.show.' . $id;
        $cacheDuration = 60 * 15; // Cache for 15 minutes

        $product = Cache::remember($cacheKey, $cacheDuration, function () use ($id) {
            return Product::with('reviews')->findOrFail($id);
        });

        return response()->json($product);
    }
}
</php>

Cache Invalidation: A critical aspect of caching is invalidation. When data changes, the cache must be updated or cleared. For the index method, you might invalidate the cache when a product is created, updated, or deleted. For the show method, invalidating the specific product’s cache is sufficient.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;

class Product extends Model
{
    use HasFactory;

    // ... other model properties and methods

    protected static function booted()
    {
        static::created(function ($product) {
            // Invalidate all products cache if a new one is created
            Cache::forget('products.all.' . md5(json_encode(request()->all()))); // This is a simplification; a more robust approach might be needed
            // A better approach for 'products.all' might be to use a tag or a wildcard flush if supported by your Redis setup.
        });

        static::updated(function ($product) {
            // Invalidate specific product cache
            Cache::forget('product.show.' . $product->id);
            // Invalidate the general list cache if relevant
            Cache::forget('products.all.' . md5(json_encode(request()->all())));
        });

        static::deleted(function ($product) {
            // Invalidate specific product cache
            Cache::forget('product.show.' . $product->id);
            // Invalidate the general list cache if relevant
            Cache::forget('products.all.' . md5(json_encode(request()->all())));
        });
    }
}
</php>

Note on products.all invalidation: Directly invalidating a cache key based on md5(json_encode(request()->all())) is fragile. A more robust strategy involves using cache tags or a dedicated cache invalidation service. For instance, if you have a consistent set of query parameters for your product list, you could define a fixed cache key for that specific query. For broader invalidation, consider using Redis’s SCAN command with a pattern (though this can be resource-intensive) or implementing a pub/sub mechanism for cache invalidation.

Rate Limiting with Redis

Laravel’s built-in rate limiter is powered by Redis, providing an efficient way to protect your API endpoints from abuse.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;

class ApiController extends Controller
{
    public function sensitiveEndpoint(Request $request)
    {
        // Limit to 100 requests per minute per IP address
        RateLimiter::for('api', function (Request $request) {
            return RateLimiter::perMinute(100)->by($request->ip());
        });

        // If the rate limit is exceeded, Laravel will automatically return a 429 response.
        // If we reach here, the request is allowed.

        // ... your API logic
        return response()->json(['message' => 'Access granted']);
    }

    public function anotherEndpoint(Request $request)
    {
        // Limit to 5 requests per hour per authenticated user
        RateLimiter::for('another_api', function (Request $request) {
            return RateLimiter::perHour(5)->by($request->user()->id);
        });

        // ... your API logic
        return response()->json(['message' => 'Access granted']);
    }
}
</php>

The rate limiter configuration is typically defined in app/Providers/RouteServiceProvider.php within the configureRateLimiting method. The RateLimiter::for('key', Closure $callback) method registers a rate limiter. The key (‘api’, ‘another_api’) is used to identify the limiter. The callback defines the limit and the identifier (e.g., IP address, user ID).

Edge Caching with Cloudflare Workers

While Redis provides server-side caching, edge caching at the CDN level significantly reduces latency for global users and offloads traffic from your origin servers. Cloudflare Workers allow you to run JavaScript at the edge, enabling sophisticated caching logic without modifying your origin application.

Setting Up a Basic Cloudflare Worker for Caching

Cloudflare Workers operate on the principle of intercepting requests and responses. We can use them to cache static assets and even dynamic API responses based on certain criteria.

Worker Script (index.js):

/**
 * Welcome to Cloudflare Workers! This is the entry point for your worker.
 *
 * On initial development, run `wrangler dev` in your terminal and visit your
 * local instance at http://localhost:8787/.
 *
 * Learn more at: https://developers.cloudflare.com/workers/
 */

// Configuration
const CACHE_TTL_SECONDS = 60 * 60 * 24; // 24 hours
const ASSET_CACHE_TTL_SECONDS = 60 * 60 * 24 * 30; // 30 days for static assets
const API_CACHE_PATHS = ['/api/products', '/api/users']; // Paths to cache as API responses
const ASSET_EXTENSIONS = ['.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'];

// Helper to generate a cache key
function generateCacheKey(request) {
    // Use URL and headers to create a unique key.
    // For simplicity, we'll use the URL. For more advanced scenarios,
    // consider including relevant headers (e.g., Accept-Language, User-Agent).
    return request.url;
}

// Helper to check if a path is an API endpoint we want to cache
function isApiEndpoint(url) {
    return API_CACHE_PATHS.some(path => url.pathname.startsWith(path));
}

// Helper to check if a path is a static asset
function isStaticAsset(url) {
    return ASSET_EXTENSIONS.some(ext => url.pathname.endsWith(ext));
}

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
    const url = new URL(request.url);
    const cacheKey = generateCacheKey(request);

    // Use Cache API for edge caching
    const cache = caches.default;

    // 1. Check cache first
    let response = await cache.match(request);

    if (response) {
        console.log(`Cache hit for: ${request.url}`);
        return response;
    }

    console.log(`Cache miss for: ${request.url}`);

    // 2. If not in cache, fetch from origin
    // IMPORTANT: Replace 'YOUR_ORIGIN_HOSTNAME' with your actual origin server's hostname.
    // You might also need to set appropriate headers for the origin request.
    const originRequest = new Request(request, {
        headers: {
            'X-Forwarded-Proto': url.protocol.slice(0, -1), // 'http' or 'https'
            'X-Forwarded-Host': url.hostname,
            // Add any other headers your origin expects
        }
    });

    response = await fetch(originRequest);

    // 3. If the response is successful and cacheable, store it in cache
    if (response.ok) {
        let ttl = CACHE_TTL_SECONDS;

        if (isStaticAsset(url)) {
            ttl = ASSET_CACHE_TTL_SECONDS;
            console.log(`Caching static asset for ${ttl} seconds: ${request.url}`);
        } else if (isApiEndpoint(url) && request.method === 'GET') {
            // Cache GET API responses
            ttl = CACHE_TTL_SECONDS;
            console.log(`Caching API response for ${ttl} seconds: ${request.url}`);
        } else {
            // For other responses, do not cache by default or use a shorter TTL
            // You might want to inspect response.headers['cache-control'] here
            // and respect origin caching directives.
            console.log(`Not caching response or using default TTL: ${request.url}`);
            // If you don't want to cache non-API/non-asset GET requests, you can return response directly here.
            // For this example, we'll cache with default TTL if it's a GET request.
            if (request.method !== 'GET') {
                return response; // Don't cache non-GET requests
            }
        }

        // Clone the response to store in cache and return to client
        const clonedResponse = response.clone();
        event.waitUntil(
            cache.put(cacheKey, clonedResponse.clone()).then(() => {
                // Set Cache-Control headers on the response sent to the client
                // This informs browsers and other intermediaries about caching policies.
                const cacheControlHeaders = {
                    'Cache-Control': `public, max-age=${ttl}, s-maxage=${ttl}`,
                    'X-Cache-Status': 'HIT', // Custom header to indicate edge cache hit
                };
                // If it's a static asset, add a longer cache directive
                if (isStaticAsset(url)) {
                    cacheControlHeaders['Cache-Control'] = `public, max-age=${ASSET_CACHE_TTL_SECONDS}, s-maxage=${ASSET_CACHE_TTL_SECONDS}, immutable`;
                }
                // For API responses, ensure they are public and have the correct max-age
                if (isApiEndpoint(url) && request.method === 'GET') {
                    cacheControlHeaders['Cache-Control'] = `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=${CACHE_TTL_SECONDS}`;
                }

                // Add custom headers to the response being sent back to the client
                // Note: We are modifying the *original* response object here before returning it.
                // The `clonedResponse` was put into the cache.
                response.headers.set('Cache-Control', cacheControlHeaders['Cache-Control']);
                response.headers.set('X-Cache-Status', 'MISS'); // Indicate origin fetch
                response.headers.set('X-Cache-TTL', ttl.toString());

                // If it's a static asset, add immutable directive
                if (isStaticAsset(url)) {
                    response.headers.set('Cache-Control', `${response.headers.get('Cache-Control')}, immutable`);
                }
            })
        );
    } else {
        // If origin returns an error, do not cache it.
        console.error(`Origin returned error: ${response.status} for ${request.url}`);
        response.headers.set('X-Cache-Status', 'ERROR');
    }

    return response;
}

Deployment with Wrangler:

1. Install Wrangler: npm install -g wrangler

2. Create a new worker project: wrangler generate my-cache-worker

3. Navigate into the project directory: cd my-cache-worker

4. Replace the contents of index.js with the script above.

5. Configure wrangler.toml. You’ll need to set your account_id and specify the routes or zone_id to bind the worker to your domain. For a specific route:

[project]
name = "my-cache-worker"
type = "javascript"

account_id = "YOUR_CLOUDFLARE_ACCOUNT_ID"

# For a specific route (e.g., caching all requests to your Laravel app)
# Replace 'yourdomain.com/*' with your actual domain and path.
# Ensure this route is more specific than any other routes that might match.
routes = [
  { pattern = "yourdomain.com/*", zone_name = "yourdomain.com" }
]

# Alternatively, if you want to bind to a zone and handle routing within the worker:
# zone_id = "YOUR_CLOUDFLARE_ZONE_ID"
# workers_dev = true # Set to false for production deployment

# If you need to access environment variables in your worker
# [vars]
# MY_VARIABLE = "some_value"

# If you need to use KV namespaces or other bindings
# [kv_namespaces]
# BINDING_NAME = { id = "YOUR_KV_NAMESPACE_ID", preview_id = "YOUR_KV_NAMESPACE_PREVIEW_ID" }

6. Deploy the worker: wrangler deploy

Advanced Cloudflare Worker Strategies

Cache Busting: For static assets, ensure your Laravel application generates unique URLs (e.g., using versioned assets with Laravel Mix or Vite). This allows for long cache durations at the edge without users receiving stale content.

Dynamic API Caching with Vary Header: For API endpoints that serve different content based on request headers (like Accept-Language or User-Agent), you must use the Vary header. The Cloudflare Worker script should respect this. If the origin response includes Vary: Accept-Language, the Worker’s cache key should ideally incorporate the Accept-Language header, or Cloudflare will automatically handle it if the Vary header is present in the origin response.

// Inside handleRequest, after fetching from origin:

if (response.ok) {
    // ... existing logic ...

    // Clone the response to store in cache and return to client
    const clonedResponse = response.clone();

    // Ensure Vary header is respected by the cache
    if (response.headers.has('Vary')) {
        console.log(`Origin response includes Vary header: ${response.headers.get('Vary')}`);
        // Cloudflare's Cache API automatically handles Vary headers if present in the origin response.
        // No explicit modification of the cache key is needed here for standard headers.
    }

    event.waitUntil(
        cache.put(cacheKey, clonedResponse.clone()).then(() => {
            // ... existing cache control headers logic ...

            // Ensure the Vary header is also present on the response sent to the client
            if (response.headers.has('Vary')) {
                response.headers.set('Vary', response.headers.get('Vary'));
            }
        })
    );
}
// ... rest of the function

Cache Invalidation via Worker Routes: You can create specific routes within your Worker to trigger cache invalidation. For example, a PURGE request to a specific endpoint could clear relevant cache entries.

// Add this to handleRequest function:

async function handleRequest(request) {
    const url = new URL(request.url);
    const cache = caches.default;

    // Handle PURGE requests for cache invalidation
    if (request.method === 'PURGE' && url.pathname === '/_cache/purge') {
        // Invalidate all cache entries. WARNING: This is a broad invalidation.
        // For granular invalidation, you'd need a more sophisticated key management.
        await cache.delete(request.url, { ignoreMethod: true }); // Delete based on URL pattern
        // A more targeted approach:
        // await cache.delete('specific_cache_key_to_purge');
        // Or using a pattern if supported by the Cache API implementation (Cloudflare's does not directly support wildcard delete by pattern)
        // For complex invalidation, consider using Cloudflare Workers KV or Durable Objects to manage cache keys.

        console.log(`Cache purged for: ${request.url}`);
        return new Response('Cache purged', { status: 204 });
    }

    // ... rest of the caching logic ...
}

To use this, you would send a PURGE request to https://yourdomain.com/_cache/purge. You’d need to secure this endpoint appropriately (e.g., with an API token or IP whitelist).

Synergistic Caching: Redis and Cloudflare Workers

The true power comes from combining these strategies. Cloudflare Workers act as the first line of defense, caching responses at the edge. If a cache miss occurs at the edge, the request hits your origin server. Here, Redis serves as a high-speed cache for database queries, expensive computations, or session data, reducing the load on your database and application logic.

Workflow Example:

  • A user requests a product page from a different continent.
  • Cloudflare Worker intercepts the request.
  • Worker checks its edge cache. Cache miss.
  • Worker forwards the request to your origin server (e.g., app.yourdomain.com).
  • Your Laravel application receives the request.
  • Laravel’s controller attempts to retrieve product data.
  • Laravel checks its Redis cache for the product data. Cache miss.
  • Laravel queries the database for the product.
  • Laravel stores the product data in Redis and returns it to the controller.
  • Laravel then returns the product data to the Cloudflare Worker.
  • The Cloudflare Worker caches the response (with appropriate Cache-Control headers) and returns it to the user.
  • Subsequent requests from users in the same region will hit the Cloudflare Worker’s cache. Requests to the origin will hit Redis first, then the database if Redis misses.

This layered approach ensures that latency is minimized for all users, while your backend infrastructure remains performant and scalable under heavy load.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Unlocking Next-Gen Performance: Advanced Caching Strategies for Laravel with Redis and Cloudflare Workers
  • Leveraging Laravel Vapor’s Serverless Architecture for Extreme Scalability and Cost Optimization in High-Traffic WordPress Headless Deployments
  • Leveraging PHP 8.3 JIT and Swoole for Real-time Laravel Microservices: A Performance Deep Dive
  • Orchestrating Microservices with Kubernetes: A Deep Dive into PHP-FPM, Laravel Queues, and MySQL Replication on AWS EKS
  • Mastering Containerized WordPress: Advanced Docker Orchestration for Scalable Headless Deployments

Categories

  • apache (1)
  • AWS (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (664)
  • Desktop Applications (14)
  • DevOps (52)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (50)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • Performance & Security Optimization (7)
  • PHP (176)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (10)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (650)
  • SEO & Growth (492)
  • Server (118)
  • Softwares (1)
  • Ubuntu (9)
  • Uncategorized (343)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (95)
  • WordPress Plugin Development (728)
  • WordPress Theme Development (357)

Recent Posts

  • Unlocking Next-Gen Performance: Advanced Caching Strategies for Laravel with Redis and Cloudflare Workers
  • Leveraging Laravel Vapor's Serverless Architecture for Extreme Scalability and Cost Optimization in High-Traffic WordPress Headless Deployments
  • Leveraging PHP 8.3 JIT and Swoole for Real-time Laravel Microservices: A Performance Deep Dive

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (728)
  • Debugging & Troubleshooting (664)
  • Security & Compliance (650)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala