Skip to content

PHP Tutorial PHP Performance and Best Practices

Caching Concepts

Lesson 341 of 369 3 min read By Deepak Paswan Updated Oct 5, 2026

Caching means storing the result of expensive work so that you can reuse it. It is the most powerful performance tool, and the one with the most ways to go wrong.

Layers of caching

LayerCachesWhere
OPcacheCompiled PHP codePHP (shared memory)
Data cacheResults of queries, API calls, calculationsFiles, APCu, Redis, Memcached
Page / fragment cacheFinished HTMLFiles, Redis, a reverse proxy
HTTP cacheResponses in the browserCache-Control, ETag headers
CDNStatic files and pages, near the visitorA CDN provider

OPcache: the one to switch on

PHP normally compiles each file to bytecode on every request. OPcache keeps the compiled version in memory, so the work happens once. It is bundled with PHP, and it is the biggest single speed-up for most sites. See Production Configuration.

A simple data cache

index.php
PHP
<?php
final class FileCache
{
    public function __construct(private string $folder)
    {
        if (!is_dir($folder)) {
            mkdir($folder, 0755, true);
        }
    }

    public function remember(string $key, int $ttl, callable $compute): mixed
    {
        $file = $this->folder . "/" . hash("sha256", $key) . ".cache";

        if (is_file($file) && filemtime($file) > time() - $ttl) {
            return unserialize(file_get_contents($file), ["allowed_classes" => false]);
        }

        $value = $compute();
        file_put_contents($file, serialize($value), LOCK_EX);

        return $value;
    }

    public function forget(string $key): void
    {
        @unlink($this->folder . "/" . hash("sha256", $key) . ".cache");
    }
}

$cache = new FileCache(__DIR__ . "/cache");
$computations = 0;

$compute = function () use (&$computations): array {
    $computations++;
    return ["top" => ["Pen", "Book"]];   // pretend this is a heavy query
};

$cache->remember("top-products", 300, $compute);
$cache->remember("top-products", 300, $compute);
$cache->forget("top-products");
$cache->remember("top-products", 300, $compute);

echo "Computed $computations times for 3 requests";
Output
OUTPUT
Computed 2 times for 3 requests

Three requests, two computations: the second one hit the cache, and after forget() the third had to compute again.

Safety notes: the cache key is hashed (so keys cannot become file paths), and unserialize() is told not to create objects (allowed_classes => false), because unserializing untrusted data can run code.

Faster stores

StoreNotes
APCuIn-memory cache inside PHP. Very fast, per server
Redis / MemcachedShared cache servers. Work across several web servers
FilesSimple, no extra software, slower
DatabasePossible, but you are using the slow thing to cache the slow thing

A common library: symfony/cache (supports all of them, and implements PSR-6 and PSR-16).

The hard part: invalidation

“There are only two hard things in computer science: cache invalidation and naming things.”

When the data changes, the cached copy is stale. You have three options:

  1. Time to live (TTL): accept that data may be up to N seconds old. Simple and often good enough.
  2. Explicit invalidation: delete the cache when the data changes (after a product update, forget("top-products")).
  3. Versioned keys: include a version in the key (products-v12). Increasing the version makes old entries unreachable.

Choose by how stale the data may be. A weather box can be five minutes old. A stock level cannot.

What to cache

Good candidates: data that is expensive to produce and requested often and changes rarely.

Do not cache:

  • Per-user private data in a shared cache without a user-specific key (users could see each other’s data).
  • Anything that must be exactly current (balances, stock during checkout).
  • Things that are already fast.

HTTP caching

Tell browsers and proxies how to cache a response.

index.php
PHP
<?php
header("Cache-Control: public, max-age=3600");     // cache for an hour

$etag = '"' . md5($content) . '"';
header("ETag: " . $etag);

if (($_SERVER["HTTP_IF_NONE_MATCH"] ?? "") === $etag) {
    http_response_code(304);   // Not Modified: no body needed
    exit;
}

For private pages, use Cache-Control: private, no-store.

WordPress

WordPress has an object cache (wp_cache_get, wp_cache_set, backed by Redis or Memcached with a plugin) and transients (see WordPress Functions). Page caching plugins save finished HTML. The same ideas apply.

Cache stampede

When a popular cache entry expires, hundreds of requests may all recompute it at the same moment. Use a short lock (only one request recomputes), or refresh the entry a little before it expires.

dangerous
Never cache secrets or private pages publicly
A page cached for one user and served to another is a data leak. Vary the cache by user, or do not cache it.