Codeskill

Learn to code, step by step

Caching strategies – opcode, data, HTTP

Onto three caching layers PHP developers actually touch: opcode cache (OPcache), application data cache, and HTTP caching headers. Used wisely, they cut response time and database load. Used blindly, they serve stale data and confuse you for hours.

Opcode cache (OPcache)

PHP compiles your scripts to opcodes before running them. OPcache keeps those opcodes in memory so repeat requests skip recompilation. On PHP 8+, it is usually enabled by default in production.

// php.ini essentials (production)
opcache.enable=1
opcache.memory_consumption=128
opcache.validate_timestamps=0   ; deploy = restart PHP-FPM to pick up code

In development, keep validate_timestamps=1 so edits apply immediately. In production, turn it off and restart PHP-FPM after deploy – faster, but you must remember the restart step.

Data cache – expensive reads

Cache the result of slow or frequent reads: category trees, settings blobs, aggregated stats. Key by a stable string; store serialised PHP or JSON with a TTL.

final class FileCache
{
    public function __construct(private string $dir) {}

    public function remember(string $key, int $ttl, callable $callback): mixed
    {
        $path = $this->dir . '/' . Hash('sha256', $key) . '.cache';
        if (is_file($path) && filemtime($path) + $ttl > time()) {
            return unserialize(file_get_contents($path));
        }
        $value = $callback();
        file_put_contents($path, serialize($value), LOCK_EX);
        return $value;
    }

    public function forget(string $key): void
    {
        $path = $this->dir . '/' . Hash('sha256', $key) . '.cache';
        if (is_file($path)) {
            unlink($path);
        }
    }
}

Usage:

$categories = $cache->remember('categories:all', 300, fn () => $repo->allCategories());

Invalidation – the hard part

There are only two hard problems in computer science, and one of them is cache invalidation. Practical rule: when data changes, delete the keys that depend on it.

public function updateCategory(int $id, array $data): void
{
    $this->pdo->prepare('UPDATE categories SET name = ? WHERE id = ?')->execute([$data['name'], $id]);
    $this->cache->forget('categories:all');
    $this->cache->forget("categories:{$id}");
}

Tag-based invalidation (flush everything tagged categories) scales better but needs Redis or similar. Start explicit; optimise when pain appears.

HTTP caching

Tell browsers and CDNs what they may cache. Static assets: long Cache-Control. Dynamic HTML: usually no-store or short TTL with careful ETag / Last-Modified.

// immutable hashed asset (app.a1b2c3.js)
header('Cache-Control: public, max-age=31536000, immutable');

// private dashboard page
header('Cache-Control: private, no-store');

// cacheable public page with revalidation
header('Cache-Control: public, max-age=60, stale-while-revalidate=30');
header('ETag: "' . Md5($content) . '"');

What not to cache

  • Personalised pages (cart, account) unless fragments vary by user
  • Anything keyed only by URL when content depends on session
  • Query results you cannot invalidate reliably

Measure before and after. If OPcache is on and you still have a problem, data or query shape is usually the culprit – not missing another cache layer.

PreviousQueues, background jobs, and long work